文件生成补充

This commit is contained in:
冯聪聪
2026-05-17 17:44:07 +08:00
parent 43b3928f16
commit db2f9d7f4b
166 changed files with 2651 additions and 3 deletions
+5
View File
@@ -29,6 +29,11 @@
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.9</version>
</dependency>
<dependency> <dependency>
<groupId>com.mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId> <artifactId>mysql-connector-j</artifactId>
@@ -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);
}
}
@@ -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);
}
};
}
}
+70
View File
@@ -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
@@ -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='图片表';
@@ -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', '最大评论层级');
@@ -0,0 +1,3 @@
fun\nojava\admin\JogApplication.class
fun\nojava\admin\config\MyBatisPlusConfig$1.class
fun\nojava\admin\config\MyBatisPlusConfig.class
@@ -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
@@ -28,6 +28,10 @@ public enum ErrorCode {
TOKEN_INVALID(1010, "Token无效"), TOKEN_INVALID(1010, "Token无效"),
TOKEN_EXPIRED(1011, "Token已过期"), TOKEN_EXPIRED(1011, "Token已过期"),
SESSION_KICKED(1012, "会话已被踢出"), SESSION_KICKED(1012, "会话已被踢出"),
USER_NOT_FOUND(1013, "用户不存在"),
PASSWORD_ERROR(1014, "密码错误"),
PERMISSION_DENIED(1015, "权限不足"),
EMAIL_EXISTS(1016, "邮箱已存在"),
ARTICLE_NOT_FOUND(2001, "文章不存在"), ARTICLE_NOT_FOUND(2001, "文章不存在"),
ARTICLE_NO_PERMISSION(2002, "无权操作该文章"), ARTICLE_NO_PERMISSION(2002, "无权操作该文章"),
@@ -41,7 +45,8 @@ public enum ErrorCode {
COMMENT_NOT_FOUND(3001, "评论不存在"), COMMENT_NOT_FOUND(3001, "评论不存在"),
COMMENT_AUDIT_PENDING(3002, "评论待审核"), COMMENT_AUDIT_PENDING(3002, "评论待审核"),
COMMENT_NO_PERMISSION(3003, "无权操作该评论"), COMMENT_NO_PERMISSION(3003, "无权操作该评论"),
COMMENT_LEVEL_EXCEEDED(3004, "评论层级超限"); COMMENT_LEVEL_EXCEEDED(3004, "评论层级超限"),
COMMENT_LEVEL_LIMIT(3005, "评论层级超限");
private final int code; private final int code;
private final String message; private final String message;
Binary file not shown.
@@ -0,0 +1,3 @@
artifactId=jog-common
groupId=fun.nojava
version=1.0.0
@@ -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
@@ -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
@@ -19,13 +19,13 @@ public class JwtTokenProvider {
private final JwtProperties jwtProperties; private final JwtProperties jwtProperties;
public String generateAccessToken(Long userId, String role) { public String generateAccessToken(Long userId, Integer role) {
Date now = new Date(); Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtProperties.getAccessTokenExpire() * 1000); Date expiryDate = new Date(now.getTime() + jwtProperties.getAccessTokenExpire() * 1000);
return Jwts.builder() return Jwts.builder()
.subject(String.valueOf(userId)) .subject(String.valueOf(userId))
.claim("role", role) .claim("role", String.valueOf(role))
.issuedAt(now) .issuedAt(now)
.expiration(expiryDate) .expiration(expiryDate)
.signWith(jwtProperties.getSigningKey()) .signWith(jwtProperties.getSigningKey())
@@ -40,4 +40,8 @@ public class TokenBlacklistService {
String key = SecurityConstants.REFRESH_TOKEN_KEY + userId; String key = SecurityConstants.REFRESH_TOKEN_KEY + userId;
redisTemplate.delete(key); redisTemplate.delete(key);
} }
public void addToBlacklist(String token) {
blacklistAccessToken(token, 7200);
}
} }
Binary file not shown.
@@ -0,0 +1,3 @@
artifactId=jog-framework
groupId=fun.nojava
version=1.0.0
@@ -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
@@ -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
@@ -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<PageResult<ArticleListVO>> 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<Void> deleteArticle(@PathVariable Long id) {
articleService.adminDeleteArticle(id);
return ApiResult.success();
}
}
@@ -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<Long> createArticle(
@AuthenticationPrincipal Long userId,
@Valid @RequestBody CreateArticleDTO dto) {
return ApiResult.success(articleService.createArticle(userId, dto));
}
@Operation(summary = "更新文章")
@PutMapping("/{id}")
public ApiResult<Void> 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<PageResult<ArticleListVO>> 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<Void> moveToRecycleBin(
@AuthenticationPrincipal Long userId,
@PathVariable Long id) {
articleService.moveToRecycleBin(userId, id);
return ApiResult.success();
}
@Operation(summary = "从回收站恢复")
@PostMapping("/{id}/restore")
public ApiResult<Void> restoreArticle(
@AuthenticationPrincipal Long userId,
@PathVariable Long id) {
articleService.restoreArticle(userId, id);
return ApiResult.success();
}
@Operation(summary = "删除文章")
@DeleteMapping("/{id}")
public ApiResult<Void> deleteArticle(
@AuthenticationPrincipal Long userId,
@PathVariable Long id) {
articleService.deleteArticle(userId, id);
return ApiResult.success();
}
}
@@ -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<List<Category>> 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<Category> createCategory(
@RequestParam String name,
@RequestParam(required = false) String description) {
return ApiResult.success(categoryService.createCategory(name, description));
}
@Operation(summary = "更新分类")
@PutMapping("/{id}")
public ApiResult<Void> 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<Void> deleteCategory(@PathVariable Long id) {
categoryService.deleteCategory(id);
return ApiResult.success();
}
}
@@ -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<PageResult<ArticleListVO>> 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<ArticleVO> getArticle(
@PathVariable Long id,
@AuthenticationPrincipal Long userId) {
articleService.incrementViewCount(id);
return ApiResult.success(articleService.getArticleDetail(id, userId));
}
@Operation(summary = "点赞文章")
@PostMapping("/{id}/like")
public ApiResult<Boolean> toggleLike(
@PathVariable Long id,
@AuthenticationPrincipal Long userId) {
return ApiResult.success(articleService.toggleLike(userId, id));
}
}
@@ -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;
}
@@ -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<String> tags;
private Long userId;
private String authorNickname;
private String authorAvatar;
private LocalDateTime publishedAt;
private LocalDateTime createdAt;
private Boolean liked;
}
@@ -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<Long> tagIds;
private Integer status = 0;
private Integer visibility = 0;
}
@@ -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<Long> tagIds;
private Integer status;
private Integer visibility;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<ArticleLike> {
}
@@ -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<Article> {
}
@@ -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<ArticleTag> {
}
@@ -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<Category> {
}
@@ -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<Tag> {
}
@@ -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<ArticleListVO> listPublicArticles(int page, int pageSize, Long categoryId, Long tagId, String keyword);
PageResult<ArticleListVO> listUserArticles(Long userId, int page, int pageSize, Integer status);
PageResult<ArticleListVO> 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);
}
@@ -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<Category> listAllCategories();
Category createCategory(String name, String description);
void updateCategory(Long id, String name, String description, Boolean enabled);
void deleteCategory(Long id);
}
@@ -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<ArticleTag>().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<ArticleListVO> listPublicArticles(int page, int pageSize, Long categoryId, Long tagId, String keyword) {
LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
.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<Long> articleIds = articleTagMapper.selectList(
new LambdaQueryWrapper<ArticleTag>().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<Article> articlePage = articleMapper.selectPage(new Page<>(page, pageSize), wrapper);
List<ArticleListVO> records = articlePage.getRecords().stream()
.map(this::buildArticleListVO)
.toList();
return new PageResult<>(records, articlePage.getTotal(), page, pageSize);
}
@Override
public PageResult<ArticleListVO> listUserArticles(Long userId, int page, int pageSize, Integer status) {
LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
.eq(Article::getUserId, userId);
if (status != null) {
wrapper.eq(Article::getStatus, status);
}
wrapper.orderByDesc(Article::getCreatedAt);
Page<Article> articlePage = articleMapper.selectPage(new Page<>(page, pageSize), wrapper);
List<ArticleListVO> records = articlePage.getRecords().stream()
.map(this::buildArticleListVO)
.toList();
return new PageResult<>(records, articlePage.getTotal(), page, pageSize);
}
@Override
public PageResult<ArticleListVO> listAdminArticles(int page, int pageSize, Integer status, Long categoryId, String keyword) {
LambdaQueryWrapper<Article> 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<Article> articlePage = articleMapper.selectPage(new Page<>(page, pageSize), wrapper);
List<ArticleListVO> 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<ArticleLike>()
.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<ArticleLike>()
.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<String> tags = articleTagMapper.selectList(
new LambdaQueryWrapper<ArticleTag>().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();
}
}
@@ -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<Category> listAllCategories() {
return categoryMapper.selectList(
new LambdaQueryWrapper<Category>()
.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);
}
}
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More