diff --git a/jog-admin/pom.xml b/jog-admin/pom.xml
index 98d2955..283c1c2 100644
--- a/jog-admin/pom.xml
+++ b/jog-admin/pom.xml
@@ -29,6 +29,11 @@
com.baomidou
mybatis-plus-spring-boot3-starter
+
+ com.baomidou
+ mybatis-plus-jsqlparser
+ 3.5.9
+
com.mysql
mysql-connector-j
diff --git a/jog-admin/src/main/java/fun/nojava/admin/JogApplication.java b/jog-admin/src/main/java/fun/nojava/admin/JogApplication.java
new file mode 100644
index 0000000..032081d
--- /dev/null
+++ b/jog-admin/src/main/java/fun/nojava/admin/JogApplication.java
@@ -0,0 +1,12 @@
+package fun.nojava.admin;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class JogApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(JogApplication.class, args);
+ }
+}
diff --git a/jog-admin/src/main/java/fun/nojava/admin/config/MyBatisPlusConfig.java b/jog-admin/src/main/java/fun/nojava/admin/config/MyBatisPlusConfig.java
new file mode 100644
index 0000000..613051a
--- /dev/null
+++ b/jog-admin/src/main/java/fun/nojava/admin/config/MyBatisPlusConfig.java
@@ -0,0 +1,38 @@
+package fun.nojava.admin.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.LocalDateTime;
+
+@Configuration
+public class MyBatisPlusConfig {
+
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+ return interceptor;
+ }
+
+ @Bean
+ public MetaObjectHandler metaObjectHandler() {
+ return new MetaObjectHandler() {
+ @Override
+ public void insertFill(MetaObject metaObject) {
+ this.strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
+ this.strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
+ }
+
+ @Override
+ public void updateFill(MetaObject metaObject) {
+ this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
+ }
+ };
+ }
+}
diff --git a/jog-admin/target/classes/application.yml b/jog-admin/target/classes/application.yml
new file mode 100644
index 0000000..2484461
--- /dev/null
+++ b/jog-admin/target/classes/application.yml
@@ -0,0 +1,70 @@
+server:
+ port: 8080
+ servlet:
+ context-path: /
+
+spring:
+ application:
+ name: jog-system
+ datasource:
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ url: jdbc:mysql://localhost:3306/jog_db?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+ username: root
+ password: root
+ data:
+ redis:
+ host: localhost
+ port: 6379
+ database: 0
+ timeout: 5000ms
+ lettuce:
+ pool:
+ max-active: 20
+ max-idle: 10
+ min-idle: 5
+ mail:
+ host: smtp.example.com
+ port: 587
+ username: noreply@example.com
+ password: your-mail-password
+ properties:
+ mail:
+ smtp:
+ auth: true
+ starttls:
+ enable: true
+ flyway:
+ enabled: true
+ locations: classpath:db/migration
+ baseline-on-migrate: true
+ encoding: UTF-8
+
+mybatis-plus:
+ mapper-locations: classpath*:/mapper/**/*.xml
+ configuration:
+ map-underscore-to-camel-case: true
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+ global-config:
+ db-config:
+ logic-delete-field: deleted
+ logic-delete-value: 1
+ logic-not-delete-value: 0
+
+jwt:
+ secret: YourSuperSecretKeyForJwtTokenGenerationMustBeAtLeast256BitsLongForHS256Algorithm
+ access-token-expire: 7200
+ refresh-token-expire: 86400
+ refresh-token-expire-remember: 1209600
+
+springdoc:
+ api-docs:
+ enabled: true
+ path: /v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger-ui.html
+
+logging:
+ level:
+ fun.nojava: DEBUG
+ org.springframework.security: DEBUG
diff --git a/jog-admin/target/classes/db/migration/V1__init_schema.sql b/jog-admin/target/classes/db/migration/V1__init_schema.sql
new file mode 100644
index 0000000..2f9b638
--- /dev/null
+++ b/jog-admin/target/classes/db/migration/V1__init_schema.sql
@@ -0,0 +1,207 @@
+-- V1__init_schema.sql
+-- 博客系统数据库初始化脚本
+
+CREATE DATABASE IF NOT EXISTS blog_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE blog_db;
+
+-- 用户表
+CREATE TABLE IF NOT EXISTS sys_user (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(50) DEFAULT NULL COMMENT '用户名',
+ email VARCHAR(100) NOT NULL COMMENT '邮箱',
+ phone VARCHAR(20) DEFAULT NULL COMMENT '手机号',
+ password VARCHAR(255) NOT NULL COMMENT '密码',
+ nickname VARCHAR(50) DEFAULT NULL COMMENT '昵称',
+ avatar VARCHAR(500) DEFAULT NULL COMMENT '头像URL',
+ user_type TINYINT NOT NULL DEFAULT 1 COMMENT '用户类型:0管理员 1普通用户',
+ status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0正常 1锁定',
+ locked_until DATETIME DEFAULT NULL COMMENT '锁定截止时间',
+ login_fail_count INT NOT NULL DEFAULT 0 COMMENT '登录失败次数',
+ email_verified TINYINT(1) NOT NULL DEFAULT 0 COMMENT '邮箱是否验证',
+ last_login_ip VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP',
+ last_login_time DATETIME DEFAULT NULL COMMENT '最后登录时间',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
+ UNIQUE KEY uk_email (email),
+ UNIQUE KEY uk_username (username),
+ KEY idx_phone (phone)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
+
+-- 用户会话表
+CREATE TABLE IF NOT EXISTS sys_user_session (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ user_id BIGINT NOT NULL COMMENT '用户ID',
+ token VARCHAR(500) NOT NULL COMMENT '访问令牌',
+ device_fingerprint VARCHAR(100) DEFAULT NULL COMMENT '设备指纹',
+ ip_address VARCHAR(50) DEFAULT NULL COMMENT 'IP地址',
+ user_agent VARCHAR(500) DEFAULT NULL COMMENT 'User-Agent',
+ expire_time DATETIME NOT NULL COMMENT '过期时间',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ KEY idx_user_id (user_id),
+ KEY idx_token (token(255))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户会话表';
+
+-- 登录日志表
+CREATE TABLE IF NOT EXISTS sys_login_log (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ user_id BIGINT DEFAULT NULL COMMENT '用户ID',
+ username VARCHAR(100) DEFAULT NULL COMMENT '登录账号',
+ login_type TINYINT NOT NULL COMMENT '登录类型:0管理员 1用户',
+ result TINYINT NOT NULL COMMENT '结果:0失败 1成功',
+ fail_reason VARCHAR(200) DEFAULT NULL COMMENT '失败原因',
+ ip_address VARCHAR(50) DEFAULT NULL COMMENT 'IP地址',
+ device_fingerprint VARCHAR(100) DEFAULT NULL COMMENT '设备指纹',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ KEY idx_user_id (user_id),
+ KEY idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录日志表';
+
+-- 用户关注表
+CREATE TABLE IF NOT EXISTS sys_user_follow (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ follower_id BIGINT NOT NULL COMMENT '关注者ID',
+ followee_id BIGINT NOT NULL COMMENT '被关注者ID',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_follow (follower_id, followee_id),
+ KEY idx_followee (followee_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户关注表';
+
+-- 通知表
+CREATE TABLE IF NOT EXISTS sys_notification (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ receiver_id BIGINT NOT NULL COMMENT '接收者ID',
+ sender_id BIGINT DEFAULT NULL COMMENT '发送者ID',
+ type TINYINT NOT NULL COMMENT '类型:0回复 1点赞 2系统',
+ content VARCHAR(500) NOT NULL COMMENT '内容',
+ related_id BIGINT DEFAULT NULL COMMENT '关联ID',
+ is_read TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已读',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ KEY idx_receiver (receiver_id),
+ KEY idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知表';
+
+-- 系统配置表
+CREATE TABLE IF NOT EXISTS sys_config (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ config_key VARCHAR(100) NOT NULL COMMENT '配置键',
+ config_value VARCHAR(500) NOT NULL COMMENT '配置值',
+ description VARCHAR(200) DEFAULT NULL COMMENT '描述',
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_config_key (config_key)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
+
+-- 敏感词表
+CREATE TABLE IF NOT EXISTS sys_sensitive_word (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ word VARCHAR(50) NOT NULL COMMENT '敏感词',
+ replacement VARCHAR(50) DEFAULT '*' COMMENT '替换词',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_word (word)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='敏感词表';
+
+-- 分类表
+CREATE TABLE IF NOT EXISTS blog_category (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(50) NOT NULL COMMENT '分类名称',
+ description VARCHAR(200) DEFAULT NULL COMMENT '描述',
+ enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
+ sort INT NOT NULL DEFAULT 0 COMMENT '排序',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_name (name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='分类表';
+
+-- 标签表
+CREATE TABLE IF NOT EXISTS blog_tag (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(50) NOT NULL COMMENT '标签名称',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_name (name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签表';
+
+-- 文章表
+CREATE TABLE IF NOT EXISTS blog_article (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ user_id BIGINT NOT NULL COMMENT '作者ID',
+ category_id BIGINT DEFAULT NULL COMMENT '分类ID',
+ title VARCHAR(200) NOT NULL COMMENT '标题',
+ summary VARCHAR(500) DEFAULT NULL COMMENT '摘要',
+ content LONGTEXT NOT NULL COMMENT '内容',
+ cover_image VARCHAR(500) DEFAULT NULL COMMENT '封面图URL',
+ status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0草稿 1已发布 2回收站',
+ visibility TINYINT NOT NULL DEFAULT 0 COMMENT '可见性:0公开 1私密 2仅粉丝',
+ view_count INT NOT NULL DEFAULT 0 COMMENT '浏览量',
+ like_count INT NOT NULL DEFAULT 0 COMMENT '点赞数',
+ comment_count INT NOT NULL DEFAULT 0 COMMENT '评论数',
+ allow_comment TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许评论',
+ published_at DATETIME DEFAULT NULL COMMENT '发布时间',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
+ KEY idx_user_id (user_id),
+ KEY idx_category_id (category_id),
+ KEY idx_status (status),
+ KEY idx_published_at (published_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章表';
+
+-- 文章标签关联表
+CREATE TABLE IF NOT EXISTS blog_article_tag (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ article_id BIGINT NOT NULL COMMENT '文章ID',
+ tag_id BIGINT NOT NULL COMMENT '标签ID',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_article_tag (article_id, tag_id),
+ KEY idx_tag_id (tag_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章标签关联表';
+
+-- 文章点赞表
+CREATE TABLE IF NOT EXISTS blog_article_like (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ article_id BIGINT NOT NULL COMMENT '文章ID',
+ user_id BIGINT NOT NULL COMMENT '用户ID',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_article_user (article_id, user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章点赞表';
+
+-- 评论表
+CREATE TABLE IF NOT EXISTS blog_comment (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ article_id BIGINT NOT NULL COMMENT '文章ID',
+ user_id BIGINT NOT NULL COMMENT '评论者ID',
+ parent_id BIGINT DEFAULT NULL COMMENT '父评论ID',
+ reply_to_user_id BIGINT DEFAULT NULL COMMENT '回复目标用户ID',
+ content VARCHAR(1000) NOT NULL COMMENT '评论内容',
+ level TINYINT NOT NULL DEFAULT 1 COMMENT '评论层级',
+ status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0待审核 1已通过 2已屏蔽 3已删除',
+ like_count INT NOT NULL DEFAULT 0 COMMENT '点赞数',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
+ KEY idx_article_id (article_id),
+ KEY idx_user_id (user_id),
+ KEY idx_parent_id (parent_id),
+ KEY idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评论表';
+
+-- 评论点赞表
+CREATE TABLE IF NOT EXISTS blog_comment_like (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ comment_id BIGINT NOT NULL COMMENT '评论ID',
+ user_id BIGINT NOT NULL COMMENT '用户ID',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uk_comment_user (comment_id, user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评论点赞表';
+
+-- 图片表
+CREATE TABLE IF NOT EXISTS blog_image (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ user_id BIGINT NOT NULL COMMENT '上传者ID',
+ original_name VARCHAR(200) NOT NULL COMMENT '原始文件名',
+ stored_name VARCHAR(200) NOT NULL COMMENT '存储文件名',
+ file_path VARCHAR(500) NOT NULL COMMENT '文件路径',
+ url VARCHAR(500) NOT NULL COMMENT '访问URL',
+ file_size BIGINT NOT NULL COMMENT '文件大小(字节)',
+ mime_type VARCHAR(50) NOT NULL COMMENT 'MIME类型',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ KEY idx_user_id (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='图片表';
diff --git a/jog-admin/target/classes/db/migration/V2__init_data.sql b/jog-admin/target/classes/db/migration/V2__init_data.sql
new file mode 100644
index 0000000..57a8629
--- /dev/null
+++ b/jog-admin/target/classes/db/migration/V2__init_data.sql
@@ -0,0 +1,29 @@
+-- V2__init_data.sql
+-- 初始化数据
+
+USE blog_db;
+
+-- 插入管理员账号(密码:Admin@123)
+INSERT INTO sys_user (username, email, password, nickname, user_type, status, email_verified)
+VALUES ('admin', 'admin@blog.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '系统管理员', 0, 0, 1);
+
+-- 插入默认分类
+INSERT INTO blog_category (name, description, enabled, sort) VALUES
+('技术', '技术相关文章', 1, 1),
+('生活', '生活随笔', 1, 2),
+('读书', '读书笔记', 1, 3);
+
+-- 插入默认标签
+INSERT INTO blog_tag (name) VALUES
+('Java'),
+('Spring Boot'),
+('Vue'),
+('前端'),
+('数据库');
+
+-- 插入系统配置
+INSERT INTO sys_config (config_key, config_value, description) VALUES
+('site_name', '我的博客', '站点名称'),
+('site_description', '一个简洁的博客系统', '站点描述'),
+('comment_audit', 'false', '评论是否需要审核'),
+('max_comment_level', '3', '最大评论层级');
diff --git a/jog-admin/target/classes/fun/nojava/admin/JogApplication.class b/jog-admin/target/classes/fun/nojava/admin/JogApplication.class
new file mode 100644
index 0000000..ec610d4
Binary files /dev/null and b/jog-admin/target/classes/fun/nojava/admin/JogApplication.class differ
diff --git a/jog-admin/target/classes/fun/nojava/admin/config/MyBatisPlusConfig$1.class b/jog-admin/target/classes/fun/nojava/admin/config/MyBatisPlusConfig$1.class
new file mode 100644
index 0000000..e3baee8
Binary files /dev/null and b/jog-admin/target/classes/fun/nojava/admin/config/MyBatisPlusConfig$1.class differ
diff --git a/jog-admin/target/classes/fun/nojava/admin/config/MyBatisPlusConfig.class b/jog-admin/target/classes/fun/nojava/admin/config/MyBatisPlusConfig.class
new file mode 100644
index 0000000..a559962
Binary files /dev/null and b/jog-admin/target/classes/fun/nojava/admin/config/MyBatisPlusConfig.class differ
diff --git a/jog-admin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/jog-admin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..167b637
--- /dev/null
+++ b/jog-admin/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,3 @@
+fun\nojava\admin\JogApplication.class
+fun\nojava\admin\config\MyBatisPlusConfig$1.class
+fun\nojava\admin\config\MyBatisPlusConfig.class
diff --git a/jog-admin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/jog-admin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..6a5549e
--- /dev/null
+++ b/jog-admin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,2 @@
+E:\person\Jog\jog-admin\src\main\java\fun\nojava\admin\config\MyBatisPlusConfig.java
+E:\person\Jog\jog-admin\src\main\java\fun\nojava\admin\JogApplication.java
diff --git a/jog-common/src/main/java/fun/nojava/common/exception/ErrorCode.java b/jog-common/src/main/java/fun/nojava/common/exception/ErrorCode.java
index 3ea2b8c..0f663b4 100644
--- a/jog-common/src/main/java/fun/nojava/common/exception/ErrorCode.java
+++ b/jog-common/src/main/java/fun/nojava/common/exception/ErrorCode.java
@@ -28,6 +28,10 @@ public enum ErrorCode {
TOKEN_INVALID(1010, "Token无效"),
TOKEN_EXPIRED(1011, "Token已过期"),
SESSION_KICKED(1012, "会话已被踢出"),
+ USER_NOT_FOUND(1013, "用户不存在"),
+ PASSWORD_ERROR(1014, "密码错误"),
+ PERMISSION_DENIED(1015, "权限不足"),
+ EMAIL_EXISTS(1016, "邮箱已存在"),
ARTICLE_NOT_FOUND(2001, "文章不存在"),
ARTICLE_NO_PERMISSION(2002, "无权操作该文章"),
@@ -41,7 +45,8 @@ public enum ErrorCode {
COMMENT_NOT_FOUND(3001, "评论不存在"),
COMMENT_AUDIT_PENDING(3002, "评论待审核"),
COMMENT_NO_PERMISSION(3003, "无权操作该评论"),
- COMMENT_LEVEL_EXCEEDED(3004, "评论层级超限");
+ COMMENT_LEVEL_EXCEEDED(3004, "评论层级超限"),
+ COMMENT_LEVEL_LIMIT(3005, "评论层级超限");
private final int code;
private final String message;
diff --git a/jog-common/target/classes/fun/nojava/common/annotation/RateLimit.class b/jog-common/target/classes/fun/nojava/common/annotation/RateLimit.class
new file mode 100644
index 0000000..bb1848c
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/annotation/RateLimit.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/constant/BlogConstants.class b/jog-common/target/classes/fun/nojava/common/constant/BlogConstants.class
new file mode 100644
index 0000000..378de5b
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/constant/BlogConstants.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/constant/SecurityConstants.class b/jog-common/target/classes/fun/nojava/common/constant/SecurityConstants.class
new file mode 100644
index 0000000..f7b722f
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/constant/SecurityConstants.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/enums/ArticleStatus.class b/jog-common/target/classes/fun/nojava/common/enums/ArticleStatus.class
new file mode 100644
index 0000000..109537b
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/enums/ArticleStatus.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/enums/ArticleVisibility.class b/jog-common/target/classes/fun/nojava/common/enums/ArticleVisibility.class
new file mode 100644
index 0000000..602d730
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/enums/ArticleVisibility.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/enums/CommentStatus.class b/jog-common/target/classes/fun/nojava/common/enums/CommentStatus.class
new file mode 100644
index 0000000..b8cbc1c
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/enums/CommentStatus.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/enums/LoginType.class b/jog-common/target/classes/fun/nojava/common/enums/LoginType.class
new file mode 100644
index 0000000..3cf5452
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/enums/LoginType.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/enums/NotificationType.class b/jog-common/target/classes/fun/nojava/common/enums/NotificationType.class
new file mode 100644
index 0000000..3589bb9
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/enums/NotificationType.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/enums/UserStatus.class b/jog-common/target/classes/fun/nojava/common/enums/UserStatus.class
new file mode 100644
index 0000000..a21c18a
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/enums/UserStatus.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/enums/UserType.class b/jog-common/target/classes/fun/nojava/common/enums/UserType.class
new file mode 100644
index 0000000..51e3ebd
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/enums/UserType.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/exception/BusinessException.class b/jog-common/target/classes/fun/nojava/common/exception/BusinessException.class
new file mode 100644
index 0000000..53cbbd3
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/exception/BusinessException.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/exception/ErrorCode.class b/jog-common/target/classes/fun/nojava/common/exception/ErrorCode.class
new file mode 100644
index 0000000..f78db2c
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/exception/ErrorCode.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/model/ApiResult.class b/jog-common/target/classes/fun/nojava/common/model/ApiResult.class
new file mode 100644
index 0000000..28ed725
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/model/ApiResult.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/model/IdVO.class b/jog-common/target/classes/fun/nojava/common/model/IdVO.class
new file mode 100644
index 0000000..b5bb3cd
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/model/IdVO.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/model/PageParam.class b/jog-common/target/classes/fun/nojava/common/model/PageParam.class
new file mode 100644
index 0000000..dfbc432
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/model/PageParam.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/model/PageResult.class b/jog-common/target/classes/fun/nojava/common/model/PageResult.class
new file mode 100644
index 0000000..9d47036
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/model/PageResult.class differ
diff --git a/jog-common/target/classes/fun/nojava/common/util/ApiResultUtils.class b/jog-common/target/classes/fun/nojava/common/util/ApiResultUtils.class
new file mode 100644
index 0000000..5378204
Binary files /dev/null and b/jog-common/target/classes/fun/nojava/common/util/ApiResultUtils.class differ
diff --git a/jog-common/target/jog-common-1.0.0.jar b/jog-common/target/jog-common-1.0.0.jar
new file mode 100644
index 0000000..4233290
Binary files /dev/null and b/jog-common/target/jog-common-1.0.0.jar differ
diff --git a/jog-common/target/maven-archiver/pom.properties b/jog-common/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..e897384
--- /dev/null
+++ b/jog-common/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=jog-common
+groupId=fun.nojava
+version=1.0.0
diff --git a/jog-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/jog-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..c2b9834
--- /dev/null
+++ b/jog-common/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,17 @@
+fun\nojava\common\enums\NotificationType.class
+fun\nojava\common\constant\BlogConstants.class
+fun\nojava\common\constant\SecurityConstants.class
+fun\nojava\common\exception\ErrorCode.class
+fun\nojava\common\exception\BusinessException.class
+fun\nojava\common\model\IdVO.class
+fun\nojava\common\enums\ArticleStatus.class
+fun\nojava\common\enums\LoginType.class
+fun\nojava\common\enums\UserStatus.class
+fun\nojava\common\model\PageResult.class
+fun\nojava\common\util\ApiResultUtils.class
+fun\nojava\common\model\PageParam.class
+fun\nojava\common\enums\CommentStatus.class
+fun\nojava\common\enums\ArticleVisibility.class
+fun\nojava\common\enums\UserType.class
+fun\nojava\common\model\ApiResult.class
+fun\nojava\common\annotation\RateLimit.class
diff --git a/jog-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/jog-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..a862ef7
--- /dev/null
+++ b/jog-common/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,17 @@
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\annotation\RateLimit.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\constant\BlogConstants.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\constant\SecurityConstants.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\enums\ArticleStatus.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\enums\ArticleVisibility.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\enums\CommentStatus.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\enums\LoginType.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\enums\NotificationType.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\enums\UserStatus.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\enums\UserType.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\exception\BusinessException.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\exception\ErrorCode.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\model\ApiResult.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\model\IdVO.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\model\PageParam.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\model\PageResult.java
+E:\person\Jog\jog-common\src\main\java\fun\nojava\common\util\ApiResultUtils.java
diff --git a/jog-framework/src/main/java/fun/nojava/framework/security/JwtTokenProvider.java b/jog-framework/src/main/java/fun/nojava/framework/security/JwtTokenProvider.java
index be45b6a..2460a3f 100644
--- a/jog-framework/src/main/java/fun/nojava/framework/security/JwtTokenProvider.java
+++ b/jog-framework/src/main/java/fun/nojava/framework/security/JwtTokenProvider.java
@@ -19,13 +19,13 @@ public class JwtTokenProvider {
private final JwtProperties jwtProperties;
- public String generateAccessToken(Long userId, String role) {
+ public String generateAccessToken(Long userId, Integer role) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtProperties.getAccessTokenExpire() * 1000);
return Jwts.builder()
.subject(String.valueOf(userId))
- .claim("role", role)
+ .claim("role", String.valueOf(role))
.issuedAt(now)
.expiration(expiryDate)
.signWith(jwtProperties.getSigningKey())
diff --git a/jog-framework/src/main/java/fun/nojava/framework/security/TokenBlacklistService.java b/jog-framework/src/main/java/fun/nojava/framework/security/TokenBlacklistService.java
index 17691c6..a87ccc6 100644
--- a/jog-framework/src/main/java/fun/nojava/framework/security/TokenBlacklistService.java
+++ b/jog-framework/src/main/java/fun/nojava/framework/security/TokenBlacklistService.java
@@ -40,4 +40,8 @@ public class TokenBlacklistService {
String key = SecurityConstants.REFRESH_TOKEN_KEY + userId;
redisTemplate.delete(key);
}
+
+ public void addToBlacklist(String token) {
+ blacklistAccessToken(token, 7200);
+ }
}
diff --git a/jog-framework/target/classes/fun/nojava/framework/aspect/LogAspect.class b/jog-framework/target/classes/fun/nojava/framework/aspect/LogAspect.class
new file mode 100644
index 0000000..0b4233c
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/aspect/LogAspect.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/aspect/RateLimitAspect.class b/jog-framework/target/classes/fun/nojava/framework/aspect/RateLimitAspect.class
new file mode 100644
index 0000000..50ffb3f
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/aspect/RateLimitAspect.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/config/JwtProperties.class b/jog-framework/target/classes/fun/nojava/framework/config/JwtProperties.class
new file mode 100644
index 0000000..9a778a2
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/config/JwtProperties.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/config/SecurityConfig.class b/jog-framework/target/classes/fun/nojava/framework/config/SecurityConfig.class
new file mode 100644
index 0000000..4e7995c
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/config/SecurityConfig.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/config/WebConfig.class b/jog-framework/target/classes/fun/nojava/framework/config/WebConfig.class
new file mode 100644
index 0000000..833e3a0
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/config/WebConfig.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/filter/XssFilter.class b/jog-framework/target/classes/fun/nojava/framework/filter/XssFilter.class
new file mode 100644
index 0000000..5186920
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/filter/XssFilter.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/handler/GlobalExceptionHandler.class b/jog-framework/target/classes/fun/nojava/framework/handler/GlobalExceptionHandler.class
new file mode 100644
index 0000000..d8bc910
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/handler/GlobalExceptionHandler.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/security/JwtAuthenticationEntryPoint.class b/jog-framework/target/classes/fun/nojava/framework/security/JwtAuthenticationEntryPoint.class
new file mode 100644
index 0000000..edb3bea
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/security/JwtAuthenticationEntryPoint.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/security/JwtAuthenticationFilter.class b/jog-framework/target/classes/fun/nojava/framework/security/JwtAuthenticationFilter.class
new file mode 100644
index 0000000..2ae8566
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/security/JwtAuthenticationFilter.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/security/JwtTokenProvider.class b/jog-framework/target/classes/fun/nojava/framework/security/JwtTokenProvider.class
new file mode 100644
index 0000000..4c9dcaf
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/security/JwtTokenProvider.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/security/TokenBlacklistService.class b/jog-framework/target/classes/fun/nojava/framework/security/TokenBlacklistService.class
new file mode 100644
index 0000000..dced970
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/security/TokenBlacklistService.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/util/WebUtils.class b/jog-framework/target/classes/fun/nojava/framework/util/WebUtils.class
new file mode 100644
index 0000000..275f608
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/util/WebUtils.class differ
diff --git a/jog-framework/target/classes/fun/nojava/framework/wrapper/XssHttpServletRequestWrapper.class b/jog-framework/target/classes/fun/nojava/framework/wrapper/XssHttpServletRequestWrapper.class
new file mode 100644
index 0000000..0e22b03
Binary files /dev/null and b/jog-framework/target/classes/fun/nojava/framework/wrapper/XssHttpServletRequestWrapper.class differ
diff --git a/jog-framework/target/jog-framework-1.0.0.jar b/jog-framework/target/jog-framework-1.0.0.jar
new file mode 100644
index 0000000..28c76e3
Binary files /dev/null and b/jog-framework/target/jog-framework-1.0.0.jar differ
diff --git a/jog-framework/target/maven-archiver/pom.properties b/jog-framework/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..c629892
--- /dev/null
+++ b/jog-framework/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=jog-framework
+groupId=fun.nojava
+version=1.0.0
diff --git a/jog-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/jog-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..1c5004e
--- /dev/null
+++ b/jog-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,13 @@
+fun\nojava\framework\security\JwtAuthenticationFilter.class
+fun\nojava\framework\util\WebUtils.class
+fun\nojava\framework\config\WebConfig.class
+fun\nojava\framework\aspect\RateLimitAspect.class
+fun\nojava\framework\security\JwtAuthenticationEntryPoint.class
+fun\nojava\framework\handler\GlobalExceptionHandler.class
+fun\nojava\framework\config\SecurityConfig.class
+fun\nojava\framework\wrapper\XssHttpServletRequestWrapper.class
+fun\nojava\framework\config\JwtProperties.class
+fun\nojava\framework\security\TokenBlacklistService.class
+fun\nojava\framework\filter\XssFilter.class
+fun\nojava\framework\security\JwtTokenProvider.class
+fun\nojava\framework\aspect\LogAspect.class
diff --git a/jog-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/jog-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..4a9078b
--- /dev/null
+++ b/jog-framework/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,13 @@
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\aspect\LogAspect.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\aspect\RateLimitAspect.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\config\JwtProperties.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\config\SecurityConfig.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\config\WebConfig.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\filter\XssFilter.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\handler\GlobalExceptionHandler.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\security\JwtAuthenticationEntryPoint.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\security\JwtAuthenticationFilter.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\security\JwtTokenProvider.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\security\TokenBlacklistService.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\util\WebUtils.java
+E:\person\Jog\jog-framework\src\main\java\fun\nojava\framework\wrapper\XssHttpServletRequestWrapper.java
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/AdminArticleController.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/AdminArticleController.java
new file mode 100644
index 0000000..166b6af
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/AdminArticleController.java
@@ -0,0 +1,37 @@
+package fun.nojava.module.blog.controller;
+
+import fun.nojava.common.model.ApiResult;
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.blog.dto.ArticleListVO;
+import fun.nojava.module.blog.service.ArticleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "管理员-文章管理")
+@RestController
+@RequestMapping("/api/admin/article")
+@RequiredArgsConstructor
+public class AdminArticleController {
+
+ private final ArticleService articleService;
+
+ @Operation(summary = "管理文章列表")
+ @GetMapping("/list")
+ public ApiResult> listArticles(
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "15") int pageSize,
+ @RequestParam(required = false) Integer status,
+ @RequestParam(required = false) Long categoryId,
+ @RequestParam(required = false) String keyword) {
+ return ApiResult.success(articleService.listAdminArticles(page, pageSize, status, categoryId, keyword));
+ }
+
+ @Operation(summary = "删除文章(管理员)")
+ @DeleteMapping("/{id}")
+ public ApiResult deleteArticle(@PathVariable Long id) {
+ articleService.adminDeleteArticle(id);
+ return ApiResult.success();
+ }
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/ArticleController.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/ArticleController.java
new file mode 100644
index 0000000..3225d42
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/ArticleController.java
@@ -0,0 +1,76 @@
+package fun.nojava.module.blog.controller;
+
+import fun.nojava.common.model.ApiResult;
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.blog.dto.*;
+import fun.nojava.module.blog.service.ArticleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "用户文章接口")
+@RestController
+@RequestMapping("/api/article")
+@RequiredArgsConstructor
+public class ArticleController {
+
+ private final ArticleService articleService;
+
+ @Operation(summary = "创建文章")
+ @PostMapping
+ public ApiResult createArticle(
+ @AuthenticationPrincipal Long userId,
+ @Valid @RequestBody CreateArticleDTO dto) {
+ return ApiResult.success(articleService.createArticle(userId, dto));
+ }
+
+ @Operation(summary = "更新文章")
+ @PutMapping("/{id}")
+ public ApiResult updateArticle(
+ @AuthenticationPrincipal Long userId,
+ @PathVariable Long id,
+ @Valid @RequestBody UpdateArticleDTO dto) {
+ articleService.updateArticle(userId, id, dto);
+ return ApiResult.success();
+ }
+
+ @Operation(summary = "我的文章列表")
+ @GetMapping("/my")
+ public ApiResult> listMyArticles(
+ @AuthenticationPrincipal Long userId,
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "10") int pageSize,
+ @RequestParam(required = false) Integer status) {
+ return ApiResult.success(articleService.listUserArticles(userId, page, pageSize, status));
+ }
+
+ @Operation(summary = "移至回收站")
+ @PostMapping("/{id}/recycle")
+ public ApiResult moveToRecycleBin(
+ @AuthenticationPrincipal Long userId,
+ @PathVariable Long id) {
+ articleService.moveToRecycleBin(userId, id);
+ return ApiResult.success();
+ }
+
+ @Operation(summary = "从回收站恢复")
+ @PostMapping("/{id}/restore")
+ public ApiResult restoreArticle(
+ @AuthenticationPrincipal Long userId,
+ @PathVariable Long id) {
+ articleService.restoreArticle(userId, id);
+ return ApiResult.success();
+ }
+
+ @Operation(summary = "删除文章")
+ @DeleteMapping("/{id}")
+ public ApiResult deleteArticle(
+ @AuthenticationPrincipal Long userId,
+ @PathVariable Long id) {
+ articleService.deleteArticle(userId, id);
+ return ApiResult.success();
+ }
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/CategoryController.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/CategoryController.java
new file mode 100644
index 0000000..b4002d8
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/CategoryController.java
@@ -0,0 +1,61 @@
+package fun.nojava.module.blog.controller;
+
+import fun.nojava.common.model.ApiResult;
+import fun.nojava.module.blog.entity.Category;
+import fun.nojava.module.blog.service.CategoryService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "分类接口")
+@RestController
+@RequestMapping("/api/public/category")
+@RequiredArgsConstructor
+class PublicCategoryController {
+
+ private final CategoryService categoryService;
+
+ @Operation(summary = "分类列表")
+ @GetMapping("/list")
+ public ApiResult> listCategories() {
+ return ApiResult.success(categoryService.listAllCategories());
+ }
+}
+
+@Tag(name = "管理员-分类管理")
+@RestController
+@RequestMapping("/api/admin/category")
+@RequiredArgsConstructor
+class AdminCategoryController {
+
+ private final CategoryService categoryService;
+
+ @Operation(summary = "创建分类")
+ @PostMapping
+ public ApiResult createCategory(
+ @RequestParam String name,
+ @RequestParam(required = false) String description) {
+ return ApiResult.success(categoryService.createCategory(name, description));
+ }
+
+ @Operation(summary = "更新分类")
+ @PutMapping("/{id}")
+ public ApiResult updateCategory(
+ @PathVariable Long id,
+ @RequestParam(required = false) String name,
+ @RequestParam(required = false) String description,
+ @RequestParam(required = false) Boolean enabled) {
+ categoryService.updateCategory(id, name, description, enabled);
+ return ApiResult.success();
+ }
+
+ @Operation(summary = "删除分类")
+ @DeleteMapping("/{id}")
+ public ApiResult deleteCategory(@PathVariable Long id) {
+ categoryService.deleteCategory(id);
+ return ApiResult.success();
+ }
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/PublicArticleController.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/PublicArticleController.java
new file mode 100644
index 0000000..0968e1e
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/controller/PublicArticleController.java
@@ -0,0 +1,49 @@
+package fun.nojava.module.blog.controller;
+
+import fun.nojava.common.model.ApiResult;
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.blog.dto.ArticleListVO;
+import fun.nojava.module.blog.dto.ArticleVO;
+import fun.nojava.module.blog.service.ArticleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "公开文章接口")
+@RestController
+@RequestMapping("/api/public/article")
+@RequiredArgsConstructor
+public class PublicArticleController {
+
+ private final ArticleService articleService;
+
+ @Operation(summary = "文章列表")
+ @GetMapping("/list")
+ public ApiResult> listArticles(
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "10") int pageSize,
+ @RequestParam(required = false) Long categoryId,
+ @RequestParam(required = false) Long tagId,
+ @RequestParam(required = false) String keyword) {
+ return ApiResult.success(articleService.listPublicArticles(page, pageSize, categoryId, tagId, keyword));
+ }
+
+ @Operation(summary = "文章详情")
+ @GetMapping("/{id}")
+ public ApiResult getArticle(
+ @PathVariable Long id,
+ @AuthenticationPrincipal Long userId) {
+ articleService.incrementViewCount(id);
+ return ApiResult.success(articleService.getArticleDetail(id, userId));
+ }
+
+ @Operation(summary = "点赞文章")
+ @PostMapping("/{id}/like")
+ public ApiResult toggleLike(
+ @PathVariable Long id,
+ @AuthenticationPrincipal Long userId) {
+ return ApiResult.success(articleService.toggleLike(userId, id));
+ }
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/ArticleListVO.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/ArticleListVO.java
new file mode 100644
index 0000000..8811208
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/ArticleListVO.java
@@ -0,0 +1,53 @@
+package fun.nojava.module.blog.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(description = "文章列表项")
+public class ArticleListVO {
+
+ @Schema(description = "文章ID")
+ private Long id;
+
+ @Schema(description = "标题")
+ private String title;
+
+ @Schema(description = "摘要")
+ private String summary;
+
+ @Schema(description = "封面图")
+ private String coverImage;
+
+ @Schema(description = "浏览量")
+ private Integer viewCount;
+
+ @Schema(description = "点赞数")
+ private Integer likeCount;
+
+ @Schema(description = "评论数")
+ private Integer commentCount;
+
+ @Schema(description = "作者ID")
+ private Long userId;
+
+ @Schema(description = "作者昵称")
+ private String authorNickname;
+
+ @Schema(description = "作者头像")
+ private String authorAvatar;
+
+ @Schema(description = "分类名称")
+ private String categoryName;
+
+ @Schema(description = "发布时间")
+ private LocalDateTime publishedAt;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/ArticleVO.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/ArticleVO.java
new file mode 100644
index 0000000..d6ae7af
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/ArticleVO.java
@@ -0,0 +1,50 @@
+package fun.nojava.module.blog.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ArticleVO {
+
+ private Long id;
+
+ private String title;
+
+ private String summary;
+
+ private String content;
+
+ private String coverImage;
+
+ private Integer viewCount;
+
+ private Integer likeCount;
+
+ private Integer commentCount;
+
+ private Long categoryId;
+
+ private String categoryName;
+
+ private List tags;
+
+ private Long userId;
+
+ private String authorNickname;
+
+ private String authorAvatar;
+
+ private LocalDateTime publishedAt;
+
+ private LocalDateTime createdAt;
+
+ private Boolean liked;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/CreateArticleDTO.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/CreateArticleDTO.java
new file mode 100644
index 0000000..f7af50c
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/CreateArticleDTO.java
@@ -0,0 +1,30 @@
+package fun.nojava.module.blog.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CreateArticleDTO {
+
+ @NotBlank(message = "标题不能为空")
+ @Size(max = 200, message = "标题最长200字符")
+ private String title;
+
+ private String summary;
+
+ @NotBlank(message = "内容不能为空")
+ private String content;
+
+ private String coverImage;
+
+ private Long categoryId;
+
+ private List tagIds;
+
+ private Integer status = 0;
+
+ private Integer visibility = 0;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/UpdateArticleDTO.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/UpdateArticleDTO.java
new file mode 100644
index 0000000..87ca976
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/dto/UpdateArticleDTO.java
@@ -0,0 +1,30 @@
+package fun.nojava.module.blog.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class UpdateArticleDTO {
+
+ @NotBlank(message = "标题不能为空")
+ @Size(max = 200, message = "标题最长200字符")
+ private String title;
+
+ private String summary;
+
+ @NotBlank(message = "内容不能为空")
+ private String content;
+
+ private String coverImage;
+
+ private Long categoryId;
+
+ private List tagIds;
+
+ private Integer status;
+
+ private Integer visibility;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Article.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Article.java
new file mode 100644
index 0000000..9bf0df7
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Article.java
@@ -0,0 +1,47 @@
+package fun.nojava.module.blog.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("article")
+public class Article {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long userId;
+
+ private Long categoryId;
+
+ private String title;
+
+ private String summary;
+
+ private String content;
+
+ private String coverImage;
+
+ private Integer status;
+
+ private Integer visibility;
+
+ private Integer viewCount;
+
+ private Integer likeCount;
+
+ private Integer commentCount;
+
+ private LocalDateTime publishedAt;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updatedAt;
+
+ @TableLogic
+ private Integer deleted;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/ArticleLike.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/ArticleLike.java
new file mode 100644
index 0000000..a9e0f9a
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/ArticleLike.java
@@ -0,0 +1,21 @@
+package fun.nojava.module.blog.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("article_like")
+public class ArticleLike {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long articleId;
+
+ private Long userId;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/ArticleTag.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/ArticleTag.java
new file mode 100644
index 0000000..a46eb5e
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/ArticleTag.java
@@ -0,0 +1,16 @@
+package fun.nojava.module.blog.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+@Data
+@TableName("article_tag")
+public class ArticleTag {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long articleId;
+
+ private Long tagId;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Category.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Category.java
new file mode 100644
index 0000000..b8ce05f
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Category.java
@@ -0,0 +1,25 @@
+package fun.nojava.module.blog.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("category")
+public class Category {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String name;
+
+ private String description;
+
+ private Integer sort;
+
+ private Boolean enabled;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Tag.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Tag.java
new file mode 100644
index 0000000..0ffcb13
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/entity/Tag.java
@@ -0,0 +1,19 @@
+package fun.nojava.module.blog.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("tag")
+public class Tag {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String name;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleLikeMapper.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleLikeMapper.java
new file mode 100644
index 0000000..56078eb
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleLikeMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.blog.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.blog.entity.ArticleLike;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ArticleLikeMapper extends BaseMapper {
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleMapper.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleMapper.java
new file mode 100644
index 0000000..f8b3657
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.blog.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.blog.entity.Article;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ArticleMapper extends BaseMapper {
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleTagMapper.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleTagMapper.java
new file mode 100644
index 0000000..ab5d998
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/ArticleTagMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.blog.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.blog.entity.ArticleTag;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ArticleTagMapper extends BaseMapper {
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/CategoryMapper.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/CategoryMapper.java
new file mode 100644
index 0000000..3077b99
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/CategoryMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.blog.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.blog.entity.Category;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CategoryMapper extends BaseMapper {
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/TagMapper.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/TagMapper.java
new file mode 100644
index 0000000..ed6a0b6
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/mapper/TagMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.blog.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.blog.entity.Tag;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TagMapper extends BaseMapper {
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/service/ArticleService.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/service/ArticleService.java
new file mode 100644
index 0000000..b9f8819
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/service/ArticleService.java
@@ -0,0 +1,33 @@
+package fun.nojava.module.blog.service;
+
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.blog.dto.*;
+
+public interface ArticleService {
+
+ Long createArticle(Long userId, CreateArticleDTO dto);
+
+ void updateArticle(Long userId, Long articleId, UpdateArticleDTO dto);
+
+ ArticleVO getArticleDetail(Long articleId, Long currentUserId);
+
+ PageResult listPublicArticles(int page, int pageSize, Long categoryId, Long tagId, String keyword);
+
+ PageResult listUserArticles(Long userId, int page, int pageSize, Integer status);
+
+ PageResult listAdminArticles(int page, int pageSize, Integer status, Long categoryId, String keyword);
+
+ void deleteArticle(Long userId, Long articleId);
+
+ void adminDeleteArticle(Long articleId);
+
+ void moveToRecycleBin(Long userId, Long articleId);
+
+ void restoreArticle(Long userId, Long articleId);
+
+ void incrementViewCount(Long articleId);
+
+ boolean toggleLike(Long userId, Long articleId);
+
+ boolean isLiked(Long userId, Long articleId);
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/service/CategoryService.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/service/CategoryService.java
new file mode 100644
index 0000000..6f9ba06
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/service/CategoryService.java
@@ -0,0 +1,16 @@
+package fun.nojava.module.blog.service;
+
+import fun.nojava.module.blog.entity.Category;
+
+import java.util.List;
+
+public interface CategoryService {
+
+ List listAllCategories();
+
+ Category createCategory(String name, String description);
+
+ void updateCategory(Long id, String name, String description, Boolean enabled);
+
+ void deleteCategory(Long id);
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/service/impl/ArticleServiceImpl.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/service/impl/ArticleServiceImpl.java
new file mode 100644
index 0000000..691e007
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/service/impl/ArticleServiceImpl.java
@@ -0,0 +1,330 @@
+package fun.nojava.module.blog.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import fun.nojava.common.enums.ArticleStatus;
+import fun.nojava.common.enums.ArticleVisibility;
+import fun.nojava.common.exception.BusinessException;
+import fun.nojava.common.exception.ErrorCode;
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.blog.dto.*;
+import fun.nojava.module.blog.entity.*;
+import fun.nojava.module.blog.mapper.*;
+import fun.nojava.module.blog.service.ArticleService;
+import fun.nojava.module.system.entity.User;
+import fun.nojava.module.system.mapper.UserMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class ArticleServiceImpl implements ArticleService {
+
+ private final ArticleMapper articleMapper;
+ private final CategoryMapper categoryMapper;
+ private final TagMapper tagMapper;
+ private final ArticleTagMapper articleTagMapper;
+ private final ArticleLikeMapper articleLikeMapper;
+ private final UserMapper userMapper;
+
+ @Override
+ @Transactional
+ public Long createArticle(Long userId, CreateArticleDTO dto) {
+ Article article = new Article();
+ article.setUserId(userId);
+ article.setCategoryId(dto.getCategoryId());
+ article.setTitle(dto.getTitle());
+ article.setSummary(dto.getSummary());
+ article.setContent(dto.getContent());
+ article.setCoverImage(dto.getCoverImage());
+ article.setStatus(dto.getStatus());
+ article.setVisibility(dto.getVisibility());
+ article.setViewCount(0);
+ article.setLikeCount(0);
+ article.setCommentCount(0);
+ article.setDeleted(0);
+
+ if (dto.getStatus() == ArticleStatus.PUBLISHED.getCode()) {
+ article.setPublishedAt(LocalDateTime.now());
+ }
+
+ articleMapper.insert(article);
+
+ if (dto.getTagIds() != null && !dto.getTagIds().isEmpty()) {
+ for (Long tagId : dto.getTagIds()) {
+ ArticleTag articleTag = new ArticleTag();
+ articleTag.setArticleId(article.getId());
+ articleTag.setTagId(tagId);
+ articleTagMapper.insert(articleTag);
+ }
+ }
+
+ return article.getId();
+ }
+
+ @Override
+ @Transactional
+ public void updateArticle(Long userId, Long articleId, UpdateArticleDTO dto) {
+ Article article = getArticleAndCheckOwner(userId, articleId);
+
+ article.setTitle(dto.getTitle());
+ article.setSummary(dto.getSummary());
+ article.setContent(dto.getContent());
+ article.setCoverImage(dto.getCoverImage());
+ article.setCategoryId(dto.getCategoryId());
+ if (dto.getStatus() != null) {
+ article.setStatus(dto.getStatus());
+ if (dto.getStatus() == ArticleStatus.PUBLISHED.getCode() && article.getPublishedAt() == null) {
+ article.setPublishedAt(LocalDateTime.now());
+ }
+ }
+ if (dto.getVisibility() != null) {
+ article.setVisibility(dto.getVisibility());
+ }
+
+ articleMapper.updateById(article);
+
+ if (dto.getTagIds() != null) {
+ articleTagMapper.delete(new LambdaQueryWrapper().eq(ArticleTag::getArticleId, articleId));
+ for (Long tagId : dto.getTagIds()) {
+ ArticleTag articleTag = new ArticleTag();
+ articleTag.setArticleId(articleId);
+ articleTag.setTagId(tagId);
+ articleTagMapper.insert(articleTag);
+ }
+ }
+ }
+
+ @Override
+ public ArticleVO getArticleDetail(Long articleId, Long currentUserId) {
+ Article article = articleMapper.selectById(articleId);
+ if (article == null) {
+ throw new BusinessException(ErrorCode.ARTICLE_NOT_FOUND);
+ }
+
+ return buildArticleVO(article, currentUserId);
+ }
+
+ @Override
+ public PageResult listPublicArticles(int page, int pageSize, Long categoryId, Long tagId, String keyword) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
+ .eq(Article::getStatus, ArticleStatus.PUBLISHED.getCode())
+ .eq(Article::getVisibility, ArticleVisibility.PUBLIC.getCode());
+
+ if (categoryId != null) {
+ wrapper.eq(Article::getCategoryId, categoryId);
+ }
+ if (keyword != null && !keyword.isEmpty()) {
+ wrapper.and(w -> w.like(Article::getTitle, keyword).or().like(Article::getSummary, keyword));
+ }
+ if (tagId != null) {
+ List articleIds = articleTagMapper.selectList(
+ new LambdaQueryWrapper().eq(ArticleTag::getTagId, tagId)
+ ).stream().map(ArticleTag::getArticleId).toList();
+ if (articleIds.isEmpty()) {
+ return new PageResult<>(Collections.emptyList(), 0, page, pageSize);
+ }
+ wrapper.in(Article::getId, articleIds);
+ }
+
+ wrapper.orderByDesc(Article::getPublishedAt);
+ Page articlePage = articleMapper.selectPage(new Page<>(page, pageSize), wrapper);
+
+ List records = articlePage.getRecords().stream()
+ .map(this::buildArticleListVO)
+ .toList();
+
+ return new PageResult<>(records, articlePage.getTotal(), page, pageSize);
+ }
+
+ @Override
+ public PageResult listUserArticles(Long userId, int page, int pageSize, Integer status) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
+ .eq(Article::getUserId, userId);
+ if (status != null) {
+ wrapper.eq(Article::getStatus, status);
+ }
+ wrapper.orderByDesc(Article::getCreatedAt);
+
+ Page articlePage = articleMapper.selectPage(new Page<>(page, pageSize), wrapper);
+ List records = articlePage.getRecords().stream()
+ .map(this::buildArticleListVO)
+ .toList();
+
+ return new PageResult<>(records, articlePage.getTotal(), page, pageSize);
+ }
+
+ @Override
+ public PageResult listAdminArticles(int page, int pageSize, Integer status, Long categoryId, String keyword) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ if (status != null) {
+ wrapper.eq(Article::getStatus, status);
+ }
+ if (categoryId != null) {
+ wrapper.eq(Article::getCategoryId, categoryId);
+ }
+ if (keyword != null && !keyword.isEmpty()) {
+ wrapper.and(w -> w.like(Article::getTitle, keyword).or().like(Article::getSummary, keyword));
+ }
+ wrapper.orderByDesc(Article::getCreatedAt);
+
+ Page articlePage = articleMapper.selectPage(new Page<>(page, pageSize), wrapper);
+ List records = articlePage.getRecords().stream()
+ .map(this::buildArticleListVO)
+ .toList();
+
+ return new PageResult<>(records, articlePage.getTotal(), page, pageSize);
+ }
+
+ @Override
+ @Transactional
+ public void deleteArticle(Long userId, Long articleId) {
+ Article article = getArticleAndCheckOwner(userId, articleId);
+ articleMapper.deleteById(articleId);
+ }
+
+ @Override
+ @Transactional
+ public void adminDeleteArticle(Long articleId) {
+ Article article = articleMapper.selectById(articleId);
+ if (article == null) {
+ throw new BusinessException(ErrorCode.ARTICLE_NOT_FOUND);
+ }
+ articleMapper.deleteById(articleId);
+ }
+
+ @Override
+ @Transactional
+ public void moveToRecycleBin(Long userId, Long articleId) {
+ Article article = getArticleAndCheckOwner(userId, articleId);
+ article.setStatus(ArticleStatus.RECYCLE.getCode());
+ article.setUpdatedAt(LocalDateTime.now());
+ articleMapper.updateById(article);
+ }
+
+ @Override
+ @Transactional
+ public void restoreArticle(Long userId, Long articleId) {
+ Article article = getArticleAndCheckOwner(userId, articleId);
+ article.setStatus(ArticleStatus.DRAFT.getCode());
+ article.setUpdatedAt(LocalDateTime.now());
+ articleMapper.updateById(article);
+ }
+
+ @Override
+ public void incrementViewCount(Long articleId) {
+ Article article = articleMapper.selectById(articleId);
+ if (article != null) {
+ article.setViewCount(article.getViewCount() + 1);
+ articleMapper.updateById(article);
+ }
+ }
+
+ @Override
+ @Transactional
+ public boolean toggleLike(Long userId, Long articleId) {
+ ArticleLike existing = articleLikeMapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(ArticleLike::getArticleId, articleId)
+ .eq(ArticleLike::getUserId, userId)
+ );
+
+ Article article = articleMapper.selectById(articleId);
+ if (article == null) {
+ throw new BusinessException(ErrorCode.ARTICLE_NOT_FOUND);
+ }
+
+ if (existing != null) {
+ articleLikeMapper.deleteById(existing.getId());
+ article.setLikeCount(article.getLikeCount() - 1);
+ articleMapper.updateById(article);
+ return false;
+ } else {
+ ArticleLike like = new ArticleLike();
+ like.setArticleId(articleId);
+ like.setUserId(userId);
+ articleLikeMapper.insert(like);
+ article.setLikeCount(article.getLikeCount() + 1);
+ articleMapper.updateById(article);
+ return true;
+ }
+ }
+
+ @Override
+ public boolean isLiked(Long userId, Long articleId) {
+ if (userId == null) return false;
+ return articleLikeMapper.selectCount(
+ new LambdaQueryWrapper()
+ .eq(ArticleLike::getArticleId, articleId)
+ .eq(ArticleLike::getUserId, userId)
+ ) > 0;
+ }
+
+ private Article getArticleAndCheckOwner(Long userId, Long articleId) {
+ Article article = articleMapper.selectById(articleId);
+ if (article == null) {
+ throw new BusinessException(ErrorCode.ARTICLE_NOT_FOUND);
+ }
+ if (!article.getUserId().equals(userId)) {
+ throw new BusinessException(ErrorCode.PERMISSION_DENIED);
+ }
+ return article;
+ }
+
+ private ArticleVO buildArticleVO(Article article, Long currentUserId) {
+ User author = userMapper.selectById(article.getUserId());
+ Category category = article.getCategoryId() != null ? categoryMapper.selectById(article.getCategoryId()) : null;
+
+ List tags = articleTagMapper.selectList(
+ new LambdaQueryWrapper().eq(ArticleTag::getArticleId, article.getId())
+ ).stream().map(at -> {
+ Tag tag = tagMapper.selectById(at.getTagId());
+ return tag != null ? tag.getName() : null;
+ }).filter(t -> t != null).toList();
+
+ return ArticleVO.builder()
+ .id(article.getId())
+ .title(article.getTitle())
+ .summary(article.getSummary())
+ .content(article.getContent())
+ .coverImage(article.getCoverImage())
+ .viewCount(article.getViewCount())
+ .likeCount(article.getLikeCount())
+ .commentCount(article.getCommentCount())
+ .categoryId(article.getCategoryId())
+ .categoryName(category != null ? category.getName() : null)
+ .tags(tags)
+ .userId(article.getUserId())
+ .authorNickname(author != null ? author.getNickname() : null)
+ .authorAvatar(author != null ? author.getAvatar() : null)
+ .publishedAt(article.getPublishedAt())
+ .createdAt(article.getCreatedAt())
+ .liked(isLiked(currentUserId, article.getId()))
+ .build();
+ }
+
+ private ArticleListVO buildArticleListVO(Article article) {
+ User author = userMapper.selectById(article.getUserId());
+ Category category = article.getCategoryId() != null ? categoryMapper.selectById(article.getCategoryId()) : null;
+
+ return ArticleListVO.builder()
+ .id(article.getId())
+ .title(article.getTitle())
+ .summary(article.getSummary())
+ .coverImage(article.getCoverImage())
+ .viewCount(article.getViewCount())
+ .likeCount(article.getLikeCount())
+ .commentCount(article.getCommentCount())
+ .userId(article.getUserId())
+ .authorNickname(author != null ? author.getNickname() : null)
+ .authorAvatar(author != null ? author.getAvatar() : null)
+ .categoryName(category != null ? category.getName() : null)
+ .publishedAt(article.getPublishedAt())
+ .build();
+ }
+}
diff --git a/jog-module-blog/src/main/java/fun/nojava/module/blog/service/impl/CategoryServiceImpl.java b/jog-module-blog/src/main/java/fun/nojava/module/blog/service/impl/CategoryServiceImpl.java
new file mode 100644
index 0000000..1c7cbc0
--- /dev/null
+++ b/jog-module-blog/src/main/java/fun/nojava/module/blog/service/impl/CategoryServiceImpl.java
@@ -0,0 +1,53 @@
+package fun.nojava.module.blog.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import fun.nojava.module.blog.entity.Category;
+import fun.nojava.module.blog.mapper.CategoryMapper;
+import fun.nojava.module.blog.service.CategoryService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class CategoryServiceImpl implements CategoryService {
+
+ private final CategoryMapper categoryMapper;
+
+ @Override
+ public List listAllCategories() {
+ return categoryMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(Category::getEnabled, true)
+ .orderByAsc(Category::getSort)
+ );
+ }
+
+ @Override
+ public Category createCategory(String name, String description) {
+ Category category = new Category();
+ category.setName(name);
+ category.setDescription(description);
+ category.setSort(0);
+ category.setEnabled(true);
+ categoryMapper.insert(category);
+ return category;
+ }
+
+ @Override
+ public void updateCategory(Long id, String name, String description, Boolean enabled) {
+ Category category = categoryMapper.selectById(id);
+ if (category != null) {
+ if (name != null) category.setName(name);
+ if (description != null) category.setDescription(description);
+ if (enabled != null) category.setEnabled(enabled);
+ categoryMapper.updateById(category);
+ }
+ }
+
+ @Override
+ public void deleteCategory(Long id) {
+ categoryMapper.deleteById(id);
+ }
+}
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/controller/AdminArticleController.class b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/AdminArticleController.class
new file mode 100644
index 0000000..188ea5f
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/AdminArticleController.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/controller/AdminCategoryController.class b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/AdminCategoryController.class
new file mode 100644
index 0000000..4924b4d
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/AdminCategoryController.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/controller/ArticleController.class b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/ArticleController.class
new file mode 100644
index 0000000..2d14db5
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/ArticleController.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/controller/PublicArticleController.class b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/PublicArticleController.class
new file mode 100644
index 0000000..87dabcc
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/PublicArticleController.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/controller/PublicCategoryController.class b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/PublicCategoryController.class
new file mode 100644
index 0000000..eb095cb
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/controller/PublicCategoryController.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleListVO$ArticleListVOBuilder.class b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleListVO$ArticleListVOBuilder.class
new file mode 100644
index 0000000..2297ea6
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleListVO$ArticleListVOBuilder.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleListVO.class b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleListVO.class
new file mode 100644
index 0000000..6f4506f
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleListVO.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleVO$ArticleVOBuilder.class b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleVO$ArticleVOBuilder.class
new file mode 100644
index 0000000..9f200e5
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleVO$ArticleVOBuilder.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleVO.class b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleVO.class
new file mode 100644
index 0000000..e09614c
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/ArticleVO.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/dto/CreateArticleDTO.class b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/CreateArticleDTO.class
new file mode 100644
index 0000000..e524c11
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/CreateArticleDTO.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/dto/UpdateArticleDTO.class b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/UpdateArticleDTO.class
new file mode 100644
index 0000000..dd33766
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/dto/UpdateArticleDTO.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Article.class b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Article.class
new file mode 100644
index 0000000..91d5efe
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Article.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/entity/ArticleLike.class b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/ArticleLike.class
new file mode 100644
index 0000000..5e9a48a
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/ArticleLike.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/entity/ArticleTag.class b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/ArticleTag.class
new file mode 100644
index 0000000..96c2fcb
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/ArticleTag.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Category.class b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Category.class
new file mode 100644
index 0000000..bc9c85f
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Category.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Tag.class b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Tag.class
new file mode 100644
index 0000000..5bf71e2
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/entity/Tag.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleLikeMapper.class b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleLikeMapper.class
new file mode 100644
index 0000000..069fd23
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleLikeMapper.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleMapper.class b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleMapper.class
new file mode 100644
index 0000000..659c45d
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleMapper.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleTagMapper.class b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleTagMapper.class
new file mode 100644
index 0000000..6f11729
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/ArticleTagMapper.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/CategoryMapper.class b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/CategoryMapper.class
new file mode 100644
index 0000000..722e1c8
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/CategoryMapper.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/TagMapper.class b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/TagMapper.class
new file mode 100644
index 0000000..115ec73
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/mapper/TagMapper.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/service/ArticleService.class b/jog-module-blog/target/classes/fun/nojava/module/blog/service/ArticleService.class
new file mode 100644
index 0000000..2d68099
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/service/ArticleService.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/service/CategoryService.class b/jog-module-blog/target/classes/fun/nojava/module/blog/service/CategoryService.class
new file mode 100644
index 0000000..eb91a12
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/service/CategoryService.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/service/impl/ArticleServiceImpl.class b/jog-module-blog/target/classes/fun/nojava/module/blog/service/impl/ArticleServiceImpl.class
new file mode 100644
index 0000000..e5a3c1b
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/service/impl/ArticleServiceImpl.class differ
diff --git a/jog-module-blog/target/classes/fun/nojava/module/blog/service/impl/CategoryServiceImpl.class b/jog-module-blog/target/classes/fun/nojava/module/blog/service/impl/CategoryServiceImpl.class
new file mode 100644
index 0000000..9e341b2
Binary files /dev/null and b/jog-module-blog/target/classes/fun/nojava/module/blog/service/impl/CategoryServiceImpl.class differ
diff --git a/jog-module-blog/target/jog-module-blog-1.0.0.jar b/jog-module-blog/target/jog-module-blog-1.0.0.jar
new file mode 100644
index 0000000..c3f2dd9
Binary files /dev/null and b/jog-module-blog/target/jog-module-blog-1.0.0.jar differ
diff --git a/jog-module-blog/target/maven-archiver/pom.properties b/jog-module-blog/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..37e075a
--- /dev/null
+++ b/jog-module-blog/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=jog-module-blog
+groupId=fun.nojava
+version=1.0.0
diff --git a/jog-module-blog/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/jog-module-blog/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..c2dd24f
--- /dev/null
+++ b/jog-module-blog/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,25 @@
+fun\nojava\module\blog\entity\ArticleTag.class
+fun\nojava\module\blog\service\ArticleService.class
+fun\nojava\module\blog\controller\PublicArticleController.class
+fun\nojava\module\blog\controller\PublicCategoryController.class
+fun\nojava\module\blog\dto\UpdateArticleDTO.class
+fun\nojava\module\blog\service\CategoryService.class
+fun\nojava\module\blog\dto\ArticleListVO.class
+fun\nojava\module\blog\entity\Category.class
+fun\nojava\module\blog\dto\CreateArticleDTO.class
+fun\nojava\module\blog\service\impl\ArticleServiceImpl.class
+fun\nojava\module\blog\dto\ArticleVO$ArticleVOBuilder.class
+fun\nojava\module\blog\controller\AdminCategoryController.class
+fun\nojava\module\blog\service\impl\CategoryServiceImpl.class
+fun\nojava\module\blog\dto\ArticleVO.class
+fun\nojava\module\blog\entity\ArticleLike.class
+fun\nojava\module\blog\controller\AdminArticleController.class
+fun\nojava\module\blog\controller\ArticleController.class
+fun\nojava\module\blog\mapper\TagMapper.class
+fun\nojava\module\blog\mapper\ArticleLikeMapper.class
+fun\nojava\module\blog\dto\ArticleListVO$ArticleListVOBuilder.class
+fun\nojava\module\blog\entity\Tag.class
+fun\nojava\module\blog\mapper\ArticleMapper.class
+fun\nojava\module\blog\entity\Article.class
+fun\nojava\module\blog\mapper\CategoryMapper.class
+fun\nojava\module\blog\mapper\ArticleTagMapper.class
diff --git a/jog-module-blog/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/jog-module-blog/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..d7177b8
--- /dev/null
+++ b/jog-module-blog/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,22 @@
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\controller\AdminArticleController.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\controller\ArticleController.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\controller\CategoryController.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\controller\PublicArticleController.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\dto\ArticleListVO.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\dto\ArticleVO.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\dto\CreateArticleDTO.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\dto\UpdateArticleDTO.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\entity\Article.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\entity\ArticleLike.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\entity\ArticleTag.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\entity\Category.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\entity\Tag.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\mapper\ArticleLikeMapper.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\mapper\ArticleMapper.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\mapper\ArticleTagMapper.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\mapper\CategoryMapper.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\mapper\TagMapper.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\service\ArticleService.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\service\CategoryService.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\service\impl\ArticleServiceImpl.java
+E:\person\Jog\jog-module-blog\src\main\java\fun\nojava\module\blog\service\impl\CategoryServiceImpl.java
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/controller/CommentController.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/controller/CommentController.java
new file mode 100644
index 0000000..1d090f1
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/controller/CommentController.java
@@ -0,0 +1,91 @@
+package fun.nojava.module.comment.controller;
+
+import fun.nojava.common.model.ApiResult;
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.comment.dto.CommentVO;
+import fun.nojava.module.comment.dto.CreateCommentDTO;
+import fun.nojava.module.comment.service.CommentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "评论接口")
+@RestController
+@RequestMapping("/api/comment")
+@RequiredArgsConstructor
+public class CommentController {
+
+ private final CommentService commentService;
+
+ @Operation(summary = "发表评论")
+ @PostMapping
+ public ApiResult createComment(
+ @AuthenticationPrincipal Long userId,
+ @Valid @RequestBody CreateCommentDTO dto) {
+ return ApiResult.success(commentService.createComment(userId, dto));
+ }
+
+ @Operation(summary = "删除评论")
+ @DeleteMapping("/{id}")
+ public ApiResult deleteComment(
+ @AuthenticationPrincipal Long userId,
+ @PathVariable Long id) {
+ commentService.deleteComment(userId, id);
+ return ApiResult.success();
+ }
+
+ @Operation(summary = "文章评论列表")
+ @GetMapping("/article/{articleId}")
+ public ApiResult> listArticleComments(
+ @PathVariable Long articleId,
+ @AuthenticationPrincipal Long userId) {
+ return ApiResult.success(commentService.listArticleComments(articleId, userId));
+ }
+
+ @Operation(summary = "点赞评论")
+ @PostMapping("/{id}/like")
+ public ApiResult toggleLike(
+ @PathVariable Long id,
+ @AuthenticationPrincipal Long userId) {
+ return ApiResult.success(commentService.toggleLike(userId, id));
+ }
+}
+
+@Tag(name = "管理员-评论管理")
+@RestController
+@RequestMapping("/api/admin/comment")
+@RequiredArgsConstructor
+class AdminCommentController {
+
+ private final CommentService commentService;
+
+ @Operation(summary = "评论列表")
+ @GetMapping("/list")
+ public ApiResult> listComments(
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "15") int pageSize,
+ @RequestParam(required = false) Integer status) {
+ return ApiResult.success(commentService.listAdminComments(page, pageSize, status));
+ }
+
+ @Operation(summary = "审核评论")
+ @PostMapping("/{id}/audit")
+ public ApiResult auditComment(
+ @PathVariable Long id,
+ @RequestParam Integer status) {
+ commentService.auditComment(id, status);
+ return ApiResult.success();
+ }
+
+ @Operation(summary = "删除评论")
+ @DeleteMapping("/{id}")
+ public ApiResult deleteComment(@PathVariable Long id) {
+ commentService.auditComment(id, 3);
+ return ApiResult.success();
+ }
+}
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/dto/CommentVO.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/dto/CommentVO.java
new file mode 100644
index 0000000..0209f59
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/dto/CommentVO.java
@@ -0,0 +1,44 @@
+package fun.nojava.module.comment.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CommentVO {
+
+ private Long id;
+
+ private Long articleId;
+
+ private Long userId;
+
+ private String nickname;
+
+ private String avatar;
+
+ private Long parentId;
+
+ private Long replyToId;
+
+ private String replyToNickname;
+
+ private String content;
+
+ private Integer status;
+
+ private Integer likeCount;
+
+ private LocalDateTime createdAt;
+
+ private Boolean liked;
+
+ private List replies;
+}
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/dto/CreateCommentDTO.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/dto/CreateCommentDTO.java
new file mode 100644
index 0000000..739ad61
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/dto/CreateCommentDTO.java
@@ -0,0 +1,19 @@
+package fun.nojava.module.comment.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class CreateCommentDTO {
+
+ @NotNull(message = "文章ID不能为空")
+ private Long articleId;
+
+ private Long parentId;
+
+ private Long replyToId;
+
+ @NotBlank(message = "评论内容不能为空")
+ private String content;
+}
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/entity/Comment.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/entity/Comment.java
new file mode 100644
index 0000000..40e73dd
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/entity/Comment.java
@@ -0,0 +1,34 @@
+package fun.nojava.module.comment.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("comment")
+public class Comment {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long articleId;
+
+ private Long userId;
+
+ private Long parentId;
+
+ private Long replyToId;
+
+ private String content;
+
+ private Integer status;
+
+ private Integer likeCount;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+
+ @TableLogic
+ private Integer deleted;
+}
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/entity/CommentLike.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/entity/CommentLike.java
new file mode 100644
index 0000000..d03da2a
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/entity/CommentLike.java
@@ -0,0 +1,21 @@
+package fun.nojava.module.comment.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("comment_like")
+public class CommentLike {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long commentId;
+
+ private Long userId;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+}
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/mapper/CommentLikeMapper.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/mapper/CommentLikeMapper.java
new file mode 100644
index 0000000..f4540e7
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/mapper/CommentLikeMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.comment.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.comment.entity.CommentLike;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CommentLikeMapper extends BaseMapper {
+}
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/mapper/CommentMapper.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/mapper/CommentMapper.java
new file mode 100644
index 0000000..3bec70b
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/mapper/CommentMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.comment.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.comment.entity.Comment;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CommentMapper extends BaseMapper {
+}
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/service/CommentService.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/service/CommentService.java
new file mode 100644
index 0000000..892401e
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/service/CommentService.java
@@ -0,0 +1,22 @@
+package fun.nojava.module.comment.service;
+
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.comment.dto.CommentVO;
+import fun.nojava.module.comment.dto.CreateCommentDTO;
+
+import java.util.List;
+
+public interface CommentService {
+
+ Long createComment(Long userId, CreateCommentDTO dto);
+
+ void deleteComment(Long userId, Long commentId);
+
+ List listArticleComments(Long articleId, Long currentUserId);
+
+ PageResult listAdminComments(int page, int pageSize, Integer status);
+
+ void auditComment(Long commentId, Integer status);
+
+ boolean toggleLike(Long userId, Long commentId);
+}
diff --git a/jog-module-comment/src/main/java/fun/nojava/module/comment/service/impl/CommentServiceImpl.java b/jog-module-comment/src/main/java/fun/nojava/module/comment/service/impl/CommentServiceImpl.java
new file mode 100644
index 0000000..879f7e3
--- /dev/null
+++ b/jog-module-comment/src/main/java/fun/nojava/module/comment/service/impl/CommentServiceImpl.java
@@ -0,0 +1,185 @@
+package fun.nojava.module.comment.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import fun.nojava.common.enums.CommentStatus;
+import fun.nojava.common.exception.BusinessException;
+import fun.nojava.common.exception.ErrorCode;
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.comment.dto.CommentVO;
+import fun.nojava.module.comment.dto.CreateCommentDTO;
+import fun.nojava.module.comment.entity.Comment;
+import fun.nojava.module.comment.entity.CommentLike;
+import fun.nojava.module.comment.mapper.CommentLikeMapper;
+import fun.nojava.module.comment.mapper.CommentMapper;
+import fun.nojava.module.comment.service.CommentService;
+import fun.nojava.module.system.entity.User;
+import fun.nojava.module.system.mapper.UserMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class CommentServiceImpl implements CommentService {
+
+ private final CommentMapper commentMapper;
+ private final CommentLikeMapper commentLikeMapper;
+ private final UserMapper userMapper;
+
+ @Override
+ @Transactional
+ public Long createComment(Long userId, CreateCommentDTO dto) {
+ if (dto.getParentId() != null) {
+ Comment parent = commentMapper.selectById(dto.getParentId());
+ if (parent == null) {
+ throw new BusinessException(ErrorCode.COMMENT_NOT_FOUND);
+ }
+ if (parent.getParentId() != null) {
+ throw new BusinessException(ErrorCode.COMMENT_LEVEL_LIMIT);
+ }
+ }
+
+ Comment comment = new Comment();
+ comment.setArticleId(dto.getArticleId());
+ comment.setUserId(userId);
+ comment.setParentId(dto.getParentId());
+ comment.setReplyToId(dto.getReplyToId());
+ comment.setContent(dto.getContent());
+ comment.setStatus(CommentStatus.APPROVED.getCode());
+ comment.setLikeCount(0);
+ comment.setDeleted(0);
+ commentMapper.insert(comment);
+
+ return comment.getId();
+ }
+
+ @Override
+ @Transactional
+ public void deleteComment(Long userId, Long commentId) {
+ Comment comment = commentMapper.selectById(commentId);
+ if (comment == null) {
+ throw new BusinessException(ErrorCode.COMMENT_NOT_FOUND);
+ }
+ if (!comment.getUserId().equals(userId)) {
+ throw new BusinessException(ErrorCode.PERMISSION_DENIED);
+ }
+ commentMapper.deleteById(commentId);
+ }
+
+ @Override
+ public List listArticleComments(Long articleId, Long currentUserId) {
+ List rootComments = commentMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(Comment::getArticleId, articleId)
+ .eq(Comment::getParentId, null)
+ .eq(Comment::getStatus, CommentStatus.APPROVED.getCode())
+ .orderByDesc(Comment::getCreatedAt)
+ );
+
+ List result = new ArrayList<>();
+ for (Comment root : rootComments) {
+ CommentVO vo = buildCommentVO(root, currentUserId);
+ List replies = commentMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(Comment::getParentId, root.getId())
+ .eq(Comment::getStatus, CommentStatus.APPROVED.getCode())
+ .orderByAsc(Comment::getCreatedAt)
+ );
+ vo.setReplies(replies.stream().map(r -> buildCommentVO(r, currentUserId)).toList());
+ result.add(vo);
+ }
+
+ return result;
+ }
+
+ @Override
+ public PageResult listAdminComments(int page, int pageSize, Integer status) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ if (status != null) {
+ wrapper.eq(Comment::getStatus, status);
+ }
+ wrapper.orderByDesc(Comment::getCreatedAt);
+
+ Page commentPage = commentMapper.selectPage(new Page<>(page, pageSize), wrapper);
+ List records = commentPage.getRecords().stream()
+ .map(c -> buildCommentVO(c, null))
+ .toList();
+
+ return new PageResult<>(records, commentPage.getTotal(), page, pageSize);
+ }
+
+ @Override
+ @Transactional
+ public void auditComment(Long commentId, Integer status) {
+ Comment comment = commentMapper.selectById(commentId);
+ if (comment == null) {
+ throw new BusinessException(ErrorCode.COMMENT_NOT_FOUND);
+ }
+ comment.setStatus(status);
+ commentMapper.updateById(comment);
+ }
+
+ @Override
+ @Transactional
+ public boolean toggleLike(Long userId, Long commentId) {
+ CommentLike existing = commentLikeMapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(CommentLike::getCommentId, commentId)
+ .eq(CommentLike::getUserId, userId)
+ );
+
+ Comment comment = commentMapper.selectById(commentId);
+ if (comment == null) {
+ throw new BusinessException(ErrorCode.COMMENT_NOT_FOUND);
+ }
+
+ if (existing != null) {
+ commentLikeMapper.deleteById(existing.getId());
+ comment.setLikeCount(comment.getLikeCount() - 1);
+ commentMapper.updateById(comment);
+ return false;
+ } else {
+ CommentLike like = new CommentLike();
+ like.setCommentId(commentId);
+ like.setUserId(userId);
+ commentLikeMapper.insert(like);
+ comment.setLikeCount(comment.getLikeCount() + 1);
+ commentMapper.updateById(comment);
+ return true;
+ }
+ }
+
+ private CommentVO buildCommentVO(Comment comment, Long currentUserId) {
+ User user = userMapper.selectById(comment.getUserId());
+ User replyToUser = comment.getReplyToId() != null ? userMapper.selectById(comment.getReplyToId()) : null;
+
+ boolean liked = false;
+ if (currentUserId != null) {
+ liked = commentLikeMapper.selectCount(
+ new LambdaQueryWrapper()
+ .eq(CommentLike::getCommentId, comment.getId())
+ .eq(CommentLike::getUserId, currentUserId)
+ ) > 0;
+ }
+
+ return CommentVO.builder()
+ .id(comment.getId())
+ .articleId(comment.getArticleId())
+ .userId(comment.getUserId())
+ .nickname(user != null ? user.getNickname() : null)
+ .avatar(user != null ? user.getAvatar() : null)
+ .parentId(comment.getParentId())
+ .replyToId(comment.getReplyToId())
+ .replyToNickname(replyToUser != null ? replyToUser.getNickname() : null)
+ .content(comment.getContent())
+ .status(comment.getStatus())
+ .likeCount(comment.getLikeCount())
+ .createdAt(comment.getCreatedAt())
+ .liked(liked)
+ .build();
+ }
+}
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/controller/AdminCommentController.class b/jog-module-comment/target/classes/fun/nojava/module/comment/controller/AdminCommentController.class
new file mode 100644
index 0000000..ca98dbc
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/controller/AdminCommentController.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/controller/CommentController.class b/jog-module-comment/target/classes/fun/nojava/module/comment/controller/CommentController.class
new file mode 100644
index 0000000..c7caeba
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/controller/CommentController.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CommentVO$CommentVOBuilder.class b/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CommentVO$CommentVOBuilder.class
new file mode 100644
index 0000000..12f71a6
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CommentVO$CommentVOBuilder.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CommentVO.class b/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CommentVO.class
new file mode 100644
index 0000000..7a5a23f
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CommentVO.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CreateCommentDTO.class b/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CreateCommentDTO.class
new file mode 100644
index 0000000..59e2689
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/dto/CreateCommentDTO.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/entity/Comment.class b/jog-module-comment/target/classes/fun/nojava/module/comment/entity/Comment.class
new file mode 100644
index 0000000..20ca366
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/entity/Comment.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/entity/CommentLike.class b/jog-module-comment/target/classes/fun/nojava/module/comment/entity/CommentLike.class
new file mode 100644
index 0000000..a8901cb
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/entity/CommentLike.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/mapper/CommentLikeMapper.class b/jog-module-comment/target/classes/fun/nojava/module/comment/mapper/CommentLikeMapper.class
new file mode 100644
index 0000000..2770ddb
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/mapper/CommentLikeMapper.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/mapper/CommentMapper.class b/jog-module-comment/target/classes/fun/nojava/module/comment/mapper/CommentMapper.class
new file mode 100644
index 0000000..ffcd297
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/mapper/CommentMapper.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/service/CommentService.class b/jog-module-comment/target/classes/fun/nojava/module/comment/service/CommentService.class
new file mode 100644
index 0000000..6d62074
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/service/CommentService.class differ
diff --git a/jog-module-comment/target/classes/fun/nojava/module/comment/service/impl/CommentServiceImpl.class b/jog-module-comment/target/classes/fun/nojava/module/comment/service/impl/CommentServiceImpl.class
new file mode 100644
index 0000000..dcf4da9
Binary files /dev/null and b/jog-module-comment/target/classes/fun/nojava/module/comment/service/impl/CommentServiceImpl.class differ
diff --git a/jog-module-comment/target/jog-module-comment-1.0.0.jar b/jog-module-comment/target/jog-module-comment-1.0.0.jar
new file mode 100644
index 0000000..23f17ba
Binary files /dev/null and b/jog-module-comment/target/jog-module-comment-1.0.0.jar differ
diff --git a/jog-module-comment/target/maven-archiver/pom.properties b/jog-module-comment/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..2dc3503
--- /dev/null
+++ b/jog-module-comment/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=jog-module-comment
+groupId=fun.nojava
+version=1.0.0
diff --git a/jog-module-comment/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/jog-module-comment/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..c96eb1c
--- /dev/null
+++ b/jog-module-comment/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,11 @@
+fun\nojava\module\comment\dto\CreateCommentDTO.class
+fun\nojava\module\comment\dto\CommentVO.class
+fun\nojava\module\comment\entity\Comment.class
+fun\nojava\module\comment\entity\CommentLike.class
+fun\nojava\module\comment\controller\CommentController.class
+fun\nojava\module\comment\service\CommentService.class
+fun\nojava\module\comment\service\impl\CommentServiceImpl.class
+fun\nojava\module\comment\dto\CommentVO$CommentVOBuilder.class
+fun\nojava\module\comment\mapper\CommentLikeMapper.class
+fun\nojava\module\comment\mapper\CommentMapper.class
+fun\nojava\module\comment\controller\AdminCommentController.class
diff --git a/jog-module-comment/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/jog-module-comment/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..d35b798
--- /dev/null
+++ b/jog-module-comment/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,9 @@
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\controller\CommentController.java
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\dto\CommentVO.java
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\dto\CreateCommentDTO.java
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\entity\Comment.java
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\entity\CommentLike.java
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\mapper\CommentLikeMapper.java
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\mapper\CommentMapper.java
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\service\CommentService.java
+E:\person\Jog\jog-module-comment\src\main\java\fun\nojava\module\comment\service\impl\CommentServiceImpl.java
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/controller/AuthController.java b/jog-module-system/src/main/java/fun/nojava/module/system/controller/AuthController.java
new file mode 100644
index 0000000..4406ba9
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/controller/AuthController.java
@@ -0,0 +1,62 @@
+package fun.nojava.module.system.controller;
+
+import fun.nojava.common.annotation.RateLimit;
+import fun.nojava.common.model.ApiResult;
+import fun.nojava.framework.util.WebUtils;
+import fun.nojava.module.system.dto.LoginDTO;
+import fun.nojava.module.system.dto.LoginVO;
+import fun.nojava.module.system.dto.RegisterDTO;
+import fun.nojava.module.system.service.AuthService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "认证接口")
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+ private final AuthService authService;
+
+ @Operation(summary = "用户登录")
+ @PostMapping("/login")
+ @RateLimit(key = "login", maxCount = 5, duration = 60)
+ public ApiResult login(@Valid @RequestBody LoginDTO dto) {
+ LoginVO vo = authService.login(dto, WebUtils.getClientIp(), WebUtils.getUserAgent());
+ return ApiResult.success(vo);
+ }
+
+ @Operation(summary = "管理员登录")
+ @PostMapping("/admin/login")
+ @RateLimit(key = "admin_login", maxCount = 5, duration = 60)
+ public ApiResult adminLogin(@Valid @RequestBody LoginDTO dto) {
+ LoginVO vo = authService.adminLogin(dto, WebUtils.getClientIp(), WebUtils.getUserAgent());
+ return ApiResult.success(vo);
+ }
+
+ @Operation(summary = "用户注册")
+ @PostMapping("/register")
+ @RateLimit(key = "register", maxCount = 3, duration = 3600)
+ public ApiResult register(@Valid @RequestBody RegisterDTO dto) {
+ authService.register(dto);
+ return ApiResult.success();
+ }
+
+ @Operation(summary = "刷新Token")
+ @PostMapping("/refresh")
+ public ApiResult refreshToken(@RequestHeader("Refresh-Token") String refreshToken) {
+ LoginVO vo = authService.refreshToken(refreshToken);
+ return ApiResult.success(vo);
+ }
+
+ @Operation(summary = "退出登录")
+ @PostMapping("/logout")
+ public ApiResult logout(@RequestHeader("Authorization") String authHeader) {
+ String token = authHeader.replace("Bearer ", "");
+ authService.logout(token);
+ return ApiResult.success();
+ }
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/controller/UserController.java b/jog-module-system/src/main/java/fun/nojava/module/system/controller/UserController.java
new file mode 100644
index 0000000..8feafa5
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/controller/UserController.java
@@ -0,0 +1,64 @@
+package fun.nojava.module.system.controller;
+
+import fun.nojava.common.model.ApiResult;
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.system.dto.UserInfoVO;
+import fun.nojava.module.system.service.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "用户接口")
+@RestController
+@RequestMapping("/api/user")
+@RequiredArgsConstructor
+public class UserController {
+
+ private final UserService userService;
+
+ @Operation(summary = "获取当前用户信息")
+ @GetMapping("/info")
+ public ApiResult getCurrentUser(@AuthenticationPrincipal Long userId) {
+ return ApiResult.success(userService.getUserInfo(userId));
+ }
+
+ @Operation(summary = "获取用户信息")
+ @GetMapping("/{id}")
+ public ApiResult getUserInfo(@PathVariable Long id) {
+ return ApiResult.success(userService.getUserInfo(id));
+ }
+}
+
+@Tag(name = "管理员-用户管理")
+@RestController
+@RequestMapping("/api/admin/user")
+@RequiredArgsConstructor
+class AdminUserController {
+
+ private final UserService userService;
+
+ @Operation(summary = "用户列表")
+ @GetMapping("/list")
+ public ApiResult> listUsers(
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "15") int pageSize,
+ @RequestParam(required = false) String keyword) {
+ return ApiResult.success(userService.listUsers(page, pageSize, keyword));
+ }
+
+ @Operation(summary = "重置密码")
+ @PostMapping("/{id}/reset-password")
+ public ApiResult resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
+ userService.resetPassword(id, newPassword);
+ return ApiResult.success();
+ }
+
+ @Operation(summary = "更新状态")
+ @PostMapping("/{id}/status")
+ public ApiResult updateStatus(@PathVariable Long id, @RequestParam int status) {
+ userService.updateUserStatus(id, status);
+ return ApiResult.success();
+ }
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/dto/LoginDTO.java b/jog-module-system/src/main/java/fun/nojava/module/system/dto/LoginDTO.java
new file mode 100644
index 0000000..0fa9245
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/dto/LoginDTO.java
@@ -0,0 +1,20 @@
+package fun.nojava.module.system.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class LoginDTO {
+
+ @NotBlank(message = "邮箱不能为空")
+ @Email(message = "邮箱格式不正确")
+ private String email;
+
+ @NotBlank(message = "密码不能为空")
+ @Size(min = 6, max = 20, message = "密码长度6-20位")
+ private String password;
+
+ private Boolean rememberMe = false;
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/dto/LoginVO.java b/jog-module-system/src/main/java/fun/nojava/module/system/dto/LoginVO.java
new file mode 100644
index 0000000..d91961a
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/dto/LoginVO.java
@@ -0,0 +1,27 @@
+package fun.nojava.module.system.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoginVO {
+
+ private Long userId;
+
+ private String email;
+
+ private String nickname;
+
+ private String avatar;
+
+ private Integer type;
+
+ private String accessToken;
+
+ private String refreshToken;
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/dto/RegisterDTO.java b/jog-module-system/src/main/java/fun/nojava/module/system/dto/RegisterDTO.java
new file mode 100644
index 0000000..64ba365
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/dto/RegisterDTO.java
@@ -0,0 +1,22 @@
+package fun.nojava.module.system.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class RegisterDTO {
+
+ @NotBlank(message = "邮箱不能为空")
+ @Email(message = "邮箱格式不正确")
+ private String email;
+
+ @NotBlank(message = "密码不能为空")
+ @Size(min = 6, max = 20, message = "密码长度6-20位")
+ private String password;
+
+ @NotBlank(message = "昵称不能为空")
+ @Size(max = 50, message = "昵称最长50字符")
+ private String nickname;
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/dto/UserInfoVO.java b/jog-module-system/src/main/java/fun/nojava/module/system/dto/UserInfoVO.java
new file mode 100644
index 0000000..513e197
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/dto/UserInfoVO.java
@@ -0,0 +1,33 @@
+package fun.nojava.module.system.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserInfoVO {
+
+ private Long id;
+
+ private String email;
+
+ private String nickname;
+
+ private String avatar;
+
+ private String bio;
+
+ private Integer type;
+
+ private Integer status;
+
+ private Integer articleCount;
+
+ private Integer followerCount;
+
+ private Integer followingCount;
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/entity/LoginLog.java b/jog-module-system/src/main/java/fun/nojava/module/system/entity/LoginLog.java
new file mode 100644
index 0000000..e2bc1e1
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/entity/LoginLog.java
@@ -0,0 +1,31 @@
+package fun.nojava.module.system.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("login_log")
+public class LoginLog {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long userId;
+
+ private String email;
+
+ private Integer loginType;
+
+ private String ipAddress;
+
+ private String userAgent;
+
+ private Integer status;
+
+ private String failReason;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/entity/User.java b/jog-module-system/src/main/java/fun/nojava/module/system/entity/User.java
new file mode 100644
index 0000000..782adc6
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/entity/User.java
@@ -0,0 +1,37 @@
+package fun.nojava.module.system.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("user")
+public class User {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String email;
+
+ private String password;
+
+ private String nickname;
+
+ private String avatar;
+
+ private String bio;
+
+ private Integer type;
+
+ private Integer status;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updatedAt;
+
+ @TableLogic
+ private Integer deleted;
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/entity/UserSession.java b/jog-module-system/src/main/java/fun/nojava/module/system/entity/UserSession.java
new file mode 100644
index 0000000..141e7f6
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/entity/UserSession.java
@@ -0,0 +1,27 @@
+package fun.nojava.module.system.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("user_session")
+public class UserSession {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long userId;
+
+ private String refreshToken;
+
+ private String deviceInfo;
+
+ private String ipAddress;
+
+ private LocalDateTime expiresAt;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createdAt;
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/mapper/LoginLogMapper.java b/jog-module-system/src/main/java/fun/nojava/module/system/mapper/LoginLogMapper.java
new file mode 100644
index 0000000..1a758fb
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/mapper/LoginLogMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.system.entity.LoginLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface LoginLogMapper extends BaseMapper {
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/mapper/UserMapper.java b/jog-module-system/src/main/java/fun/nojava/module/system/mapper/UserMapper.java
new file mode 100644
index 0000000..d0da547
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/mapper/UserMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.system.entity.User;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface UserMapper extends BaseMapper {
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/mapper/UserSessionMapper.java b/jog-module-system/src/main/java/fun/nojava/module/system/mapper/UserSessionMapper.java
new file mode 100644
index 0000000..0862959
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/mapper/UserSessionMapper.java
@@ -0,0 +1,9 @@
+package fun.nojava.module.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import fun.nojava.module.system.entity.UserSession;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface UserSessionMapper extends BaseMapper {
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/service/AuthService.java b/jog-module-system/src/main/java/fun/nojava/module/system/service/AuthService.java
new file mode 100644
index 0000000..768be18
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/service/AuthService.java
@@ -0,0 +1,20 @@
+package fun.nojava.module.system.service;
+
+import fun.nojava.module.system.dto.LoginDTO;
+import fun.nojava.module.system.dto.LoginVO;
+import fun.nojava.module.system.dto.RegisterDTO;
+
+public interface AuthService {
+
+ LoginVO login(LoginDTO dto, String ipAddress, String userAgent);
+
+ LoginVO adminLogin(LoginDTO dto, String ipAddress, String userAgent);
+
+ void register(RegisterDTO dto);
+
+ LoginVO refreshToken(String refreshToken);
+
+ void logout(String accessToken);
+
+ void logoutAll(Long userId);
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/service/UserService.java b/jog-module-system/src/main/java/fun/nojava/module/system/service/UserService.java
new file mode 100644
index 0000000..973fa56
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/service/UserService.java
@@ -0,0 +1,24 @@
+package fun.nojava.module.system.service;
+
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.system.dto.UserInfoVO;
+import fun.nojava.module.system.entity.User;
+
+public interface UserService {
+
+ UserInfoVO getUserInfo(Long userId);
+
+ User getById(Long id);
+
+ PageResult listUsers(int page, int pageSize, String keyword);
+
+ void resetPassword(Long userId, String newPassword);
+
+ void updateUserStatus(Long userId, int status);
+
+ boolean isFollowing(Long followerId, Long followeeId);
+
+ void follow(Long followerId, Long followeeId);
+
+ void unfollow(Long followerId, Long followeeId);
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/service/impl/AuthServiceImpl.java b/jog-module-system/src/main/java/fun/nojava/module/system/service/impl/AuthServiceImpl.java
new file mode 100644
index 0000000..68dd37c
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/service/impl/AuthServiceImpl.java
@@ -0,0 +1,171 @@
+package fun.nojava.module.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import fun.nojava.common.enums.UserStatus;
+import fun.nojava.common.enums.UserType;
+import fun.nojava.common.exception.BusinessException;
+import fun.nojava.common.exception.ErrorCode;
+import fun.nojava.framework.security.JwtTokenProvider;
+import fun.nojava.framework.security.TokenBlacklistService;
+import fun.nojava.module.system.dto.LoginDTO;
+import fun.nojava.module.system.dto.LoginVO;
+import fun.nojava.module.system.dto.RegisterDTO;
+import fun.nojava.module.system.entity.LoginLog;
+import fun.nojava.module.system.entity.User;
+import fun.nojava.module.system.entity.UserSession;
+import fun.nojava.module.system.mapper.LoginLogMapper;
+import fun.nojava.module.system.mapper.UserMapper;
+import fun.nojava.module.system.mapper.UserSessionMapper;
+import fun.nojava.module.system.service.AuthService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+
+@Service
+@RequiredArgsConstructor
+public class AuthServiceImpl implements AuthService {
+
+ private final UserMapper userMapper;
+ private final LoginLogMapper loginLogMapper;
+ private final UserSessionMapper userSessionMapper;
+ private final PasswordEncoder passwordEncoder;
+ private final JwtTokenProvider jwtTokenProvider;
+ private final TokenBlacklistService tokenBlacklistService;
+
+ @Override
+ @Transactional
+ public LoginVO login(LoginDTO dto, String ipAddress, String userAgent) {
+ User user = userMapper.selectOne(
+ new LambdaQueryWrapper().eq(User::getEmail, dto.getEmail())
+ );
+
+ if (user == null) {
+ recordLoginLog(null, dto.getEmail(), ipAddress, userAgent, 0, "用户不存在");
+ throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+ }
+
+ if (user.getStatus() == UserStatus.LOCKED.getCode()) {
+ recordLoginLog(user.getId(), dto.getEmail(), ipAddress, userAgent, 0, "账号已锁定");
+ throw new BusinessException(ErrorCode.ACCOUNT_LOCKED);
+ }
+
+ if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
+ recordLoginLog(user.getId(), dto.getEmail(), ipAddress, userAgent, 0, "密码错误");
+ throw new BusinessException(ErrorCode.PASSWORD_ERROR);
+ }
+
+ recordLoginLog(user.getId(), dto.getEmail(), ipAddress, userAgent, 1, null);
+
+ return generateLoginVO(user, dto.getRememberMe());
+ }
+
+ @Override
+ @Transactional
+ public LoginVO adminLogin(LoginDTO dto, String ipAddress, String userAgent) {
+ LoginVO loginVO = login(dto, ipAddress, userAgent);
+
+ User user = userMapper.selectById(loginVO.getUserId());
+ if (user.getType() != UserType.ADMIN.getCode()) {
+ throw new BusinessException(ErrorCode.PERMISSION_DENIED);
+ }
+
+ return loginVO;
+ }
+
+ @Override
+ @Transactional
+ public void register(RegisterDTO dto) {
+ Long count = userMapper.selectCount(
+ new LambdaQueryWrapper().eq(User::getEmail, dto.getEmail())
+ );
+ if (count > 0) {
+ throw new BusinessException(ErrorCode.EMAIL_EXISTS);
+ }
+
+ User user = new User();
+ user.setEmail(dto.getEmail());
+ user.setPassword(passwordEncoder.encode(dto.getPassword()));
+ user.setNickname(dto.getNickname());
+ user.setType(UserType.USER.getCode());
+ user.setStatus(UserStatus.NORMAL.getCode());
+ user.setDeleted(0);
+ userMapper.insert(user);
+ }
+
+ @Override
+ public LoginVO refreshToken(String refreshToken) {
+ if (!jwtTokenProvider.validateToken(refreshToken)) {
+ throw new BusinessException(ErrorCode.TOKEN_INVALID);
+ }
+
+ Long userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
+ User user = userMapper.selectById(userId);
+
+ if (user == null) {
+ throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+ }
+
+ String accessToken = jwtTokenProvider.generateAccessToken(userId, user.getType());
+ String newRefreshToken = jwtTokenProvider.generateRefreshToken(userId);
+
+ return LoginVO.builder()
+ .userId(userId)
+ .email(user.getEmail())
+ .nickname(user.getNickname())
+ .avatar(user.getAvatar())
+ .type(user.getType())
+ .accessToken(accessToken)
+ .refreshToken(newRefreshToken)
+ .build();
+ }
+
+ @Override
+ public void logout(String accessToken) {
+ tokenBlacklistService.addToBlacklist(accessToken);
+ }
+
+ @Override
+ public void logoutAll(Long userId) {
+ userSessionMapper.delete(
+ new LambdaQueryWrapper().eq(UserSession::getUserId, userId)
+ );
+ }
+
+ private LoginVO generateLoginVO(User user, Boolean rememberMe) {
+ String accessToken = jwtTokenProvider.generateAccessToken(user.getId(), user.getType());
+ String refreshToken = jwtTokenProvider.generateRefreshToken(user.getId());
+
+ UserSession session = new UserSession();
+ session.setUserId(user.getId());
+ session.setRefreshToken(refreshToken);
+ session.setExpiresAt(LocalDateTime.now().plusSeconds(
+ rememberMe ? 1209600L : 86400L
+ ));
+ userSessionMapper.insert(session);
+
+ return LoginVO.builder()
+ .userId(user.getId())
+ .email(user.getEmail())
+ .nickname(user.getNickname())
+ .avatar(user.getAvatar())
+ .type(user.getType())
+ .accessToken(accessToken)
+ .refreshToken(refreshToken)
+ .build();
+ }
+
+ private void recordLoginLog(Long userId, String email, String ip, String ua, int status, String reason) {
+ LoginLog log = new LoginLog();
+ log.setUserId(userId);
+ log.setEmail(email);
+ log.setLoginType(1);
+ log.setIpAddress(ip);
+ log.setUserAgent(ua);
+ log.setStatus(status);
+ log.setFailReason(reason);
+ loginLogMapper.insert(log);
+ }
+}
diff --git a/jog-module-system/src/main/java/fun/nojava/module/system/service/impl/UserServiceImpl.java b/jog-module-system/src/main/java/fun/nojava/module/system/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..16a1aa2
--- /dev/null
+++ b/jog-module-system/src/main/java/fun/nojava/module/system/service/impl/UserServiceImpl.java
@@ -0,0 +1,106 @@
+package fun.nojava.module.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import fun.nojava.common.exception.BusinessException;
+import fun.nojava.common.exception.ErrorCode;
+import fun.nojava.common.model.PageResult;
+import fun.nojava.module.system.dto.UserInfoVO;
+import fun.nojava.module.system.entity.User;
+import fun.nojava.module.system.mapper.UserMapper;
+import fun.nojava.module.system.service.UserService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class UserServiceImpl implements UserService {
+
+ private final UserMapper userMapper;
+ private final PasswordEncoder passwordEncoder;
+
+ @Override
+ public UserInfoVO getUserInfo(Long userId) {
+ User user = userMapper.selectById(userId);
+ if (user == null) {
+ throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+ }
+
+ return UserInfoVO.builder()
+ .id(user.getId())
+ .email(user.getEmail())
+ .nickname(user.getNickname())
+ .avatar(user.getAvatar())
+ .bio(user.getBio())
+ .type(user.getType())
+ .status(user.getStatus())
+ .articleCount(0)
+ .followerCount(0)
+ .followingCount(0)
+ .build();
+ }
+
+ @Override
+ public User getById(Long id) {
+ return userMapper.selectById(id);
+ }
+
+ @Override
+ public PageResult listUsers(int page, int pageSize, String keyword) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ if (keyword != null && !keyword.isEmpty()) {
+ wrapper.and(w -> w.like(User::getNickname, keyword).or().like(User::getEmail, keyword));
+ }
+ wrapper.orderByDesc(User::getCreatedAt);
+
+ Page userPage = userMapper.selectPage(new Page<>(page, pageSize), wrapper);
+ List records = userPage.getRecords().stream()
+ .map(u -> UserInfoVO.builder()
+ .id(u.getId())
+ .email(u.getEmail())
+ .nickname(u.getNickname())
+ .avatar(u.getAvatar())
+ .type(u.getType())
+ .status(u.getStatus())
+ .build())
+ .toList();
+
+ return new PageResult<>(records, userPage.getTotal(), page, pageSize);
+ }
+
+ @Override
+ public void resetPassword(Long userId, String newPassword) {
+ User user = userMapper.selectById(userId);
+ if (user == null) {
+ throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+ }
+ user.setPassword(passwordEncoder.encode(newPassword));
+ userMapper.updateById(user);
+ }
+
+ @Override
+ public void updateUserStatus(Long userId, int status) {
+ User user = userMapper.selectById(userId);
+ if (user == null) {
+ throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+ }
+ user.setStatus(status);
+ userMapper.updateById(user);
+ }
+
+ @Override
+ public boolean isFollowing(Long followerId, Long followeeId) {
+ return false;
+ }
+
+ @Override
+ public void follow(Long followerId, Long followeeId) {
+ }
+
+ @Override
+ public void unfollow(Long followerId, Long followeeId) {
+ }
+}
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/controller/AdminUserController.class b/jog-module-system/target/classes/fun/nojava/module/system/controller/AdminUserController.class
new file mode 100644
index 0000000..cf9934c
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/controller/AdminUserController.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/controller/AuthController.class b/jog-module-system/target/classes/fun/nojava/module/system/controller/AuthController.class
new file mode 100644
index 0000000..860f397
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/controller/AuthController.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/controller/UserController.class b/jog-module-system/target/classes/fun/nojava/module/system/controller/UserController.class
new file mode 100644
index 0000000..5b9895b
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/controller/UserController.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginDTO.class b/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginDTO.class
new file mode 100644
index 0000000..90bfcd8
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginDTO.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginVO$LoginVOBuilder.class b/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginVO$LoginVOBuilder.class
new file mode 100644
index 0000000..299e960
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginVO$LoginVOBuilder.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginVO.class b/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginVO.class
new file mode 100644
index 0000000..5f08874
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/dto/LoginVO.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/dto/RegisterDTO.class b/jog-module-system/target/classes/fun/nojava/module/system/dto/RegisterDTO.class
new file mode 100644
index 0000000..5725890
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/dto/RegisterDTO.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/dto/UserInfoVO$UserInfoVOBuilder.class b/jog-module-system/target/classes/fun/nojava/module/system/dto/UserInfoVO$UserInfoVOBuilder.class
new file mode 100644
index 0000000..37b18dc
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/dto/UserInfoVO$UserInfoVOBuilder.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/dto/UserInfoVO.class b/jog-module-system/target/classes/fun/nojava/module/system/dto/UserInfoVO.class
new file mode 100644
index 0000000..adc1bc9
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/dto/UserInfoVO.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/entity/LoginLog.class b/jog-module-system/target/classes/fun/nojava/module/system/entity/LoginLog.class
new file mode 100644
index 0000000..6c0f45a
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/entity/LoginLog.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/entity/User.class b/jog-module-system/target/classes/fun/nojava/module/system/entity/User.class
new file mode 100644
index 0000000..07ed2fc
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/entity/User.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/entity/UserSession.class b/jog-module-system/target/classes/fun/nojava/module/system/entity/UserSession.class
new file mode 100644
index 0000000..86bdf9e
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/entity/UserSession.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/mapper/LoginLogMapper.class b/jog-module-system/target/classes/fun/nojava/module/system/mapper/LoginLogMapper.class
new file mode 100644
index 0000000..be0ce52
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/mapper/LoginLogMapper.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/mapper/UserMapper.class b/jog-module-system/target/classes/fun/nojava/module/system/mapper/UserMapper.class
new file mode 100644
index 0000000..b3f2c85
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/mapper/UserMapper.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/mapper/UserSessionMapper.class b/jog-module-system/target/classes/fun/nojava/module/system/mapper/UserSessionMapper.class
new file mode 100644
index 0000000..6d7dd80
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/mapper/UserSessionMapper.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/service/AuthService.class b/jog-module-system/target/classes/fun/nojava/module/system/service/AuthService.class
new file mode 100644
index 0000000..f5e2124
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/service/AuthService.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/service/UserService.class b/jog-module-system/target/classes/fun/nojava/module/system/service/UserService.class
new file mode 100644
index 0000000..171a898
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/service/UserService.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/service/impl/AuthServiceImpl.class b/jog-module-system/target/classes/fun/nojava/module/system/service/impl/AuthServiceImpl.class
new file mode 100644
index 0000000..2c60a3a
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/service/impl/AuthServiceImpl.class differ
diff --git a/jog-module-system/target/classes/fun/nojava/module/system/service/impl/UserServiceImpl.class b/jog-module-system/target/classes/fun/nojava/module/system/service/impl/UserServiceImpl.class
new file mode 100644
index 0000000..7f09050
Binary files /dev/null and b/jog-module-system/target/classes/fun/nojava/module/system/service/impl/UserServiceImpl.class differ
diff --git a/jog-module-system/target/jog-module-system-1.0.0.jar b/jog-module-system/target/jog-module-system-1.0.0.jar
new file mode 100644
index 0000000..88f3880
Binary files /dev/null and b/jog-module-system/target/jog-module-system-1.0.0.jar differ
diff --git a/jog-module-system/target/maven-archiver/pom.properties b/jog-module-system/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..46ed235
--- /dev/null
+++ b/jog-module-system/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=jog-module-system
+groupId=fun.nojava
+version=1.0.0
diff --git a/jog-module-system/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/jog-module-system/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..224a856
--- /dev/null
+++ b/jog-module-system/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,19 @@
+fun\nojava\module\system\dto\LoginVO$LoginVOBuilder.class
+fun\nojava\module\system\entity\LoginLog.class
+fun\nojava\module\system\controller\AdminUserController.class
+fun\nojava\module\system\service\impl\AuthServiceImpl.class
+fun\nojava\module\system\service\UserService.class
+fun\nojava\module\system\mapper\UserSessionMapper.class
+fun\nojava\module\system\controller\AuthController.class
+fun\nojava\module\system\dto\LoginDTO.class
+fun\nojava\module\system\dto\UserInfoVO$UserInfoVOBuilder.class
+fun\nojava\module\system\dto\UserInfoVO.class
+fun\nojava\module\system\mapper\UserMapper.class
+fun\nojava\module\system\dto\RegisterDTO.class
+fun\nojava\module\system\entity\User.class
+fun\nojava\module\system\entity\UserSession.class
+fun\nojava\module\system\service\AuthService.class
+fun\nojava\module\system\dto\LoginVO.class
+fun\nojava\module\system\service\impl\UserServiceImpl.class
+fun\nojava\module\system\controller\UserController.class
+fun\nojava\module\system\mapper\LoginLogMapper.class
diff --git a/jog-module-system/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/jog-module-system/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..95f41f2
--- /dev/null
+++ b/jog-module-system/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,16 @@
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\controller\AuthController.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\controller\UserController.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\dto\LoginDTO.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\dto\LoginVO.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\dto\RegisterDTO.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\dto\UserInfoVO.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\entity\LoginLog.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\entity\User.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\entity\UserSession.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\mapper\LoginLogMapper.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\mapper\UserMapper.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\mapper\UserSessionMapper.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\service\AuthService.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\service\impl\AuthServiceImpl.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\service\impl\UserServiceImpl.java
+E:\person\Jog\jog-module-system\src\main\java\fun\nojava\module\system\service\UserService.java