文件生成补充
This commit is contained in:
@@ -29,6 +29,11 @@
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
<version>3.5.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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', '最大评论层级');
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
fun\nojava\admin\JogApplication.class
|
||||
fun\nojava\admin\config\MyBatisPlusConfig$1.class
|
||||
fun\nojava\admin\config\MyBatisPlusConfig.class
|
||||
+2
@@ -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_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;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
artifactId=jog-common
|
||||
groupId=fun.nojava
|
||||
version=1.0.0
|
||||
+17
@@ -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
|
||||
+17
@@ -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;
|
||||
|
||||
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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
artifactId=jog-framework
|
||||
groupId=fun.nojava
|
||||
version=1.0.0
|
||||
+13
@@ -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
|
||||
+13
@@ -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
|
||||
+37
@@ -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();
|
||||
}
|
||||
}
|
||||
+76
@@ -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();
|
||||
}
|
||||
}
|
||||
+61
@@ -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();
|
||||
}
|
||||
}
|
||||
+49
@@ -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);
|
||||
}
|
||||
+330
@@ -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();
|
||||
}
|
||||
}
|
||||
+53
@@ -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);
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user