mirror of
https://github.com/201206030/novel-cloud.git
synced 2025-08-24 17:42:42 +00:00
refactor: 基于 novel 项目 & Spring Cloud 2022 & Spring Cloud Alibaba 2022 重构
This commit is contained in:
54
novel-core/novel-common/pom.xml
Normal file
54
novel-core/novel-common/pom.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>io.github.xxyopen</groupId>
|
||||
<artifactId>novel-cloud</artifactId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>novel-common</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-common</artifactId>
|
||||
<version>${springdoc-openapi.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
@@ -0,0 +1,78 @@
|
||||
package io.github.xxyopen.novel.common.auth;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* JWT 工具类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/17
|
||||
*/
|
||||
@UtilityClass
|
||||
@Slf4j
|
||||
public class JwtUtils {
|
||||
|
||||
/**
|
||||
* JWT 加密密钥
|
||||
*/
|
||||
private static final String SECRET = "E66559580A1ADF48CDD928516062F12E";
|
||||
|
||||
/**
|
||||
* 定义系统标识头常量
|
||||
*/
|
||||
private static final String HEADER_SYSTEM_KEY = "systemKeyHeader";
|
||||
|
||||
/**
|
||||
* 根据用户ID生成JWT
|
||||
*
|
||||
* @param uid 用户ID
|
||||
* @param systemKey 系统标识
|
||||
* @return JWT
|
||||
*/
|
||||
public String generateToken(Long uid, String systemKey) {
|
||||
return Jwts.builder()
|
||||
.setHeaderParam(HEADER_SYSTEM_KEY, systemKey)
|
||||
.setSubject(uid.toString())
|
||||
.signWith(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)))
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析JWT返回用户ID
|
||||
*
|
||||
* @param token JWT
|
||||
* @param systemKey 系统标识
|
||||
* @return 用户ID
|
||||
*/
|
||||
public Long parseToken(String token, String systemKey) {
|
||||
Jws<Claims> claimsJws;
|
||||
try {
|
||||
claimsJws = Jwts.parserBuilder()
|
||||
.setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)))
|
||||
.build()
|
||||
.parseClaimsJws(token);
|
||||
// OK, we can trust this JWT
|
||||
// 判断该 JWT 是否属于指定系统
|
||||
if (Objects.equals(claimsJws.getHeader().get(HEADER_SYSTEM_KEY), systemKey)) {
|
||||
return Long.parseLong(claimsJws.getBody().getSubject());
|
||||
}
|
||||
} catch (JwtException e) {
|
||||
log.warn("JWT解析失败:{}", token);
|
||||
// don't trust the JWT!
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package io.github.xxyopen.novel.common.auth;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
/**
|
||||
* 用户信息 持有类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/18
|
||||
*/
|
||||
@UtilityClass
|
||||
public class UserHolder {
|
||||
|
||||
/**
|
||||
* 当前线程用户ID
|
||||
*/
|
||||
private static final ThreadLocal<Long> userIdTL = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 当前线程作家ID
|
||||
*/
|
||||
private static final ThreadLocal<Long> authorIdTL = new ThreadLocal<>();
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
userIdTL.set(userId);
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userIdTL.get();
|
||||
}
|
||||
|
||||
public void setAuthorId(Long authorId) {
|
||||
authorIdTL.set(authorId);
|
||||
}
|
||||
|
||||
public Long getAuthorId() {
|
||||
return authorIdTL.get();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
userIdTL.remove();
|
||||
authorIdTL.remove();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package io.github.xxyopen.novel.common.constant;
|
||||
|
||||
/**
|
||||
* AMQP 相关常量
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/25
|
||||
*/
|
||||
public class AmqpConsts {
|
||||
|
||||
/**
|
||||
* 小说信息改变 MQ
|
||||
*/
|
||||
public static class BookChangeMq {
|
||||
|
||||
/**
|
||||
* 小说信息改变交换机
|
||||
*/
|
||||
public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE";
|
||||
|
||||
/**
|
||||
* Elasticsearch book 索引更新的队列
|
||||
*/
|
||||
public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE";
|
||||
|
||||
/**
|
||||
* Redis book 缓存更新的队列
|
||||
*/
|
||||
public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE";
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
package io.github.xxyopen.novel.common.constant;
|
||||
|
||||
/**
|
||||
* API 路由常量
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/12
|
||||
*/
|
||||
public class ApiRouterConsts {
|
||||
|
||||
private ApiRouterConsts() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
/**
|
||||
* API请求路径前缀
|
||||
*/
|
||||
public static final String API_URL_PREFIX = "/api";
|
||||
|
||||
/**
|
||||
* 前台门户系统请求路径前缀
|
||||
*/
|
||||
public static final String API_FRONT_URL_PREFIX = API_URL_PREFIX + "/front";
|
||||
|
||||
/**
|
||||
* 作家管理系统请求路径前缀
|
||||
*/
|
||||
public static final String API_AUTHOR_URL_PREFIX = API_URL_PREFIX + "/author";
|
||||
|
||||
/**
|
||||
* 平台后台管理系统请求路径前缀
|
||||
*/
|
||||
public static final String API_ADMIN_URL_PREFIX = API_URL_PREFIX + "/admin";
|
||||
|
||||
/**
|
||||
* 微服务内部调用请求路径前缀
|
||||
*/
|
||||
public static final String API_INNER_URL_PREFIX = API_URL_PREFIX + "/inner";
|
||||
|
||||
/**
|
||||
* 首页模块请求路径前缀
|
||||
*/
|
||||
public static final String HOME_URL_PREFIX = "/home";
|
||||
|
||||
/**
|
||||
* 首页模块请求路径前缀
|
||||
*/
|
||||
public static final String NEWS_URL_PREFIX = "/news";
|
||||
|
||||
/**
|
||||
* 小说模块请求路径前缀
|
||||
*/
|
||||
public static final String BOOK_URL_PREFIX = "/book";
|
||||
|
||||
/**
|
||||
* 会员模块请求路径前缀
|
||||
*/
|
||||
public static final String USER_URL_PREFIX = "/user";
|
||||
|
||||
/**
|
||||
* 资源(图片/视频/文档)模块请求路径前缀
|
||||
*/
|
||||
public static final String RESOURCE_URL_PREFIX = "/resource";
|
||||
|
||||
/**
|
||||
* 搜索模块请求路径前缀
|
||||
*/
|
||||
public static final String SEARCH_URL_PREFIX = "/search";
|
||||
|
||||
/**
|
||||
* 前台门户首页API请求路径前缀
|
||||
*/
|
||||
public static final String API_FRONT_HOME_URL_PREFIX = API_FRONT_URL_PREFIX + HOME_URL_PREFIX;
|
||||
|
||||
/**
|
||||
* 前台门户新闻相关API请求路径前缀
|
||||
*/
|
||||
public static final String API_FRONT_NEWS_URL_PREFIX = API_FRONT_URL_PREFIX + NEWS_URL_PREFIX;
|
||||
|
||||
/**
|
||||
* 前台门户小说相关API请求路径前缀
|
||||
*/
|
||||
public static final String API_FRONT_BOOK_URL_PREFIX = API_FRONT_URL_PREFIX + BOOK_URL_PREFIX;
|
||||
|
||||
/**
|
||||
* 前台门户会员相关API请求路径前缀
|
||||
*/
|
||||
public static final String API_FRONT_USER_URL_PREFIX = API_FRONT_URL_PREFIX + USER_URL_PREFIX;
|
||||
|
||||
/**
|
||||
* 前台门户资源(图片/视频/文档)相关API请求路径前缀
|
||||
*/
|
||||
public static final String API_FRONT_RESOURCE_URL_PREFIX =
|
||||
API_FRONT_URL_PREFIX + RESOURCE_URL_PREFIX;
|
||||
|
||||
/**
|
||||
* 前台门户搜索相关API请求路径前缀
|
||||
*/
|
||||
public static final String API_FRONT_SEARCH_URL_PREFIX =
|
||||
API_FRONT_URL_PREFIX + SEARCH_URL_PREFIX;
|
||||
|
||||
/**
|
||||
* 小说微服务内部调用接口
|
||||
* */
|
||||
public static final String API_INNER_BOOK_URL_PREFIX =
|
||||
API_INNER_URL_PREFIX + BOOK_URL_PREFIX;
|
||||
|
||||
/**
|
||||
* 会员微服务内部调用接口
|
||||
* */
|
||||
public static final String API_INNER_USER_URL_PREFIX =
|
||||
API_INNER_URL_PREFIX + USER_URL_PREFIX;
|
||||
|
||||
}
|
@@ -0,0 +1,175 @@
|
||||
package io.github.xxyopen.novel.common.constant;
|
||||
|
||||
/**
|
||||
* 缓存相关常量
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/12
|
||||
*/
|
||||
public class CacheConsts {
|
||||
|
||||
/**
|
||||
* 本项目 Redis 缓存前缀
|
||||
*/
|
||||
public static final String REDIS_CACHE_PREFIX = "Cache::Novel::";
|
||||
|
||||
|
||||
/**
|
||||
* Caffeine 缓存管理器
|
||||
*/
|
||||
public static final String CAFFEINE_CACHE_MANAGER = "caffeineCacheManager";
|
||||
|
||||
/**
|
||||
* Redis 缓存管理器
|
||||
*/
|
||||
public static final String REDIS_CACHE_MANAGER = "redisCacheManager";
|
||||
|
||||
/**
|
||||
* 首页小说推荐缓存
|
||||
*/
|
||||
public static final String HOME_BOOK_CACHE_NAME = "homeBookCache";
|
||||
|
||||
/**
|
||||
* 最新新闻缓存
|
||||
*/
|
||||
public static final String LATEST_NEWS_CACHE_NAME = "latestNewsCache";
|
||||
|
||||
/**
|
||||
* 小说点击榜缓存
|
||||
*/
|
||||
public static final String BOOK_VISIT_RANK_CACHE_NAME = "bookVisitRankCache";
|
||||
|
||||
/**
|
||||
* 小说新书榜缓存
|
||||
*/
|
||||
public static final String BOOK_NEWEST_RANK_CACHE_NAME = "bookNewestRankCache";
|
||||
|
||||
/**
|
||||
* 小说更新榜缓存
|
||||
*/
|
||||
public static final String BOOK_UPDATE_RANK_CACHE_NAME = "bookUpdateRankCache";
|
||||
|
||||
/**
|
||||
* 首页友情链接缓存
|
||||
*/
|
||||
public static final String HOME_FRIEND_LINK_CACHE_NAME = "homeFriendLinkCache";
|
||||
|
||||
/**
|
||||
* 小说分类列表缓存
|
||||
*/
|
||||
public static final String BOOK_CATEGORY_LIST_CACHE_NAME = "bookCategoryListCache";
|
||||
|
||||
/**
|
||||
* 小说信息缓存
|
||||
*/
|
||||
public static final String BOOK_INFO_CACHE_NAME = "bookInfoCache";
|
||||
|
||||
/**
|
||||
* 小说章节缓存
|
||||
*/
|
||||
public static final String BOOK_CHAPTER_CACHE_NAME = "bookChapterCache";
|
||||
|
||||
/**
|
||||
* 小说内容缓存
|
||||
*/
|
||||
public static final String BOOK_CONTENT_CACHE_NAME = "bookContentCache";
|
||||
|
||||
/**
|
||||
* 最近更新小说ID列表缓存
|
||||
*/
|
||||
public static final String LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME = "lastUpdateBookIdListCache";
|
||||
|
||||
/**
|
||||
* 图片验证码缓存 KEY
|
||||
*/
|
||||
public static final String IMG_VERIFY_CODE_CACHE_KEY =
|
||||
REDIS_CACHE_PREFIX + "imgVerifyCodeCache::";
|
||||
|
||||
/**
|
||||
* 用户信息缓存
|
||||
*/
|
||||
public static final String USER_INFO_CACHE_NAME = "userInfoCache";
|
||||
|
||||
/**
|
||||
* 作家信息缓存
|
||||
*/
|
||||
public static final String AUTHOR_INFO_CACHE_NAME = "authorInfoCache";
|
||||
|
||||
/**
|
||||
* 缓存配置常量
|
||||
*/
|
||||
public enum CacheEnum {
|
||||
|
||||
HOME_BOOK_CACHE(0, HOME_BOOK_CACHE_NAME, 60 * 60 * 24, 1),
|
||||
|
||||
LATEST_NEWS_CACHE(0, LATEST_NEWS_CACHE_NAME, 60 * 10, 1),
|
||||
|
||||
BOOK_VISIT_RANK_CACHE(2, BOOK_VISIT_RANK_CACHE_NAME, 60 * 60 * 6, 1),
|
||||
|
||||
BOOK_NEWEST_RANK_CACHE(0, BOOK_NEWEST_RANK_CACHE_NAME, 60 * 30, 1),
|
||||
|
||||
BOOK_UPDATE_RANK_CACHE(0, BOOK_UPDATE_RANK_CACHE_NAME, 60, 1),
|
||||
|
||||
HOME_FRIEND_LINK_CACHE(2, HOME_FRIEND_LINK_CACHE_NAME, 0, 1),
|
||||
|
||||
BOOK_CATEGORY_LIST_CACHE(0, BOOK_CATEGORY_LIST_CACHE_NAME, 0, 2),
|
||||
|
||||
BOOK_INFO_CACHE(0, BOOK_INFO_CACHE_NAME, 60 * 60 * 18, 500),
|
||||
|
||||
BOOK_CHAPTER_CACHE(0, BOOK_CHAPTER_CACHE_NAME, 60 * 60 * 6, 5000),
|
||||
|
||||
BOOK_CONTENT_CACHE(2, BOOK_CONTENT_CACHE_NAME, 60 * 60 * 12, 3000),
|
||||
|
||||
LAST_UPDATE_BOOK_ID_LIST_CACHE(0, LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME, 60 * 60, 10),
|
||||
|
||||
USER_INFO_CACHE(2, USER_INFO_CACHE_NAME, 60 * 60 * 24, 10000),
|
||||
|
||||
AUTHOR_INFO_CACHE(2, AUTHOR_INFO_CACHE_NAME, 60 * 60 * 48, 1000);
|
||||
|
||||
/**
|
||||
* 缓存类型 0-本地 1-本地和远程 2-远程
|
||||
*/
|
||||
private int type;
|
||||
/**
|
||||
* 缓存的名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 失效时间(秒) 0-永不失效
|
||||
*/
|
||||
private int ttl;
|
||||
/**
|
||||
* 最大容量
|
||||
*/
|
||||
private int maxSize;
|
||||
|
||||
CacheEnum(int type, String name, int ttl, int maxSize) {
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.ttl = ttl;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public boolean isLocal() {
|
||||
return type <= 1;
|
||||
}
|
||||
|
||||
public boolean isRemote() {
|
||||
return type >= 1;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getTtl() {
|
||||
return ttl;
|
||||
}
|
||||
|
||||
public int getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package io.github.xxyopen.novel.common.constant;
|
||||
|
||||
|
||||
/**
|
||||
* 通用常量
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/12
|
||||
*/
|
||||
public class CommonConsts {
|
||||
|
||||
/**
|
||||
* 是
|
||||
*/
|
||||
public static final Integer YES = 1;
|
||||
public static final String TRUE = "true";
|
||||
|
||||
|
||||
/**
|
||||
* 否
|
||||
*/
|
||||
public static final Integer NO = 0;
|
||||
public static final String FALSE = "false";
|
||||
|
||||
/**
|
||||
* 性别常量
|
||||
*/
|
||||
public enum SexEnum {
|
||||
|
||||
/**
|
||||
* 男
|
||||
*/
|
||||
MALE(0, "男"),
|
||||
|
||||
/**
|
||||
* 女
|
||||
*/
|
||||
FEMALE(1, "女");
|
||||
|
||||
SexEnum(int code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
private int code;
|
||||
private String desc;
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,201 @@
|
||||
package io.github.xxyopen.novel.common.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据库 常量
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/12
|
||||
*/
|
||||
public class DatabaseConsts {
|
||||
|
||||
/**
|
||||
* 用户信息表
|
||||
*/
|
||||
public static class UserInfoTable {
|
||||
|
||||
private UserInfoTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_USERNAME = "username";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户反馈表
|
||||
*/
|
||||
public static class UserFeedBackTable {
|
||||
|
||||
private UserFeedBackTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_USER_ID = "user_id";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户书架表
|
||||
*/
|
||||
public static class UserBookshelfTable {
|
||||
|
||||
private UserBookshelfTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_USER_ID = "user_id";
|
||||
|
||||
public static final String COLUMN_BOOK_ID = "book_id";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 作家信息表
|
||||
*/
|
||||
public static class AuthorInfoTable {
|
||||
|
||||
private AuthorInfoTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_USER_ID = "user_id";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 小说类别表
|
||||
*/
|
||||
public static class BookCategoryTable {
|
||||
|
||||
private BookCategoryTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_WORK_DIRECTION = "work_direction";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 小说表
|
||||
*/
|
||||
public static class BookTable {
|
||||
|
||||
private BookTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_CATEGORY_ID = "category_id";
|
||||
|
||||
public static final String COLUMN_BOOK_NAME = "book_name";
|
||||
|
||||
public static final String AUTHOR_ID = "author_id";
|
||||
|
||||
public static final String COLUMN_VISIT_COUNT = "visit_count";
|
||||
|
||||
public static final String COLUMN_WORD_COUNT = "word_count";
|
||||
|
||||
public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 小说章节表
|
||||
*/
|
||||
public static class BookChapterTable {
|
||||
|
||||
private BookChapterTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_BOOK_ID = "book_id";
|
||||
|
||||
public static final String COLUMN_CHAPTER_NUM = "chapter_num";
|
||||
|
||||
public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 小说内容表
|
||||
*/
|
||||
public static class BookContentTable {
|
||||
|
||||
private BookContentTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_CHAPTER_ID = "chapter_id";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 小说评论表
|
||||
*/
|
||||
public static class BookCommentTable {
|
||||
|
||||
private BookCommentTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_BOOK_ID = "book_id";
|
||||
|
||||
public static final String COLUMN_USER_ID = "user_id";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 新闻内容表
|
||||
*/
|
||||
public static class NewsContentTable {
|
||||
|
||||
private NewsContentTable() {
|
||||
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
public static final String COLUMN_NEWS_ID = "news_id";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用列枚举类
|
||||
*/
|
||||
@Getter
|
||||
public enum CommonColumnEnum {
|
||||
|
||||
ID("id"),
|
||||
SORT("sort"),
|
||||
CREATE_TIME("create_time"),
|
||||
UPDATE_TIME("update_time");
|
||||
|
||||
private String name;
|
||||
|
||||
CommonColumnEnum(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* SQL语句枚举类
|
||||
*/
|
||||
@Getter
|
||||
public enum SqlEnum {
|
||||
|
||||
LIMIT_1("limit 1"),
|
||||
LIMIT_2("limit 2"),
|
||||
LIMIT_5("limit 5"),
|
||||
LIMIT_30("limit 30"),
|
||||
LIMIT_500("limit 500");
|
||||
|
||||
private String sql;
|
||||
|
||||
SqlEnum(String sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,152 @@
|
||||
package io.github.xxyopen.novel.common.constant;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 错误码枚举类。
|
||||
* <p>
|
||||
* 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。 错误产生来源分为 A/B/C, A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 超时等问题; B
|
||||
* 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题; C 表示错误来源 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的
|
||||
* 步长间距预留 100。
|
||||
* <p>
|
||||
* 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码。
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/11
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ErrorCodeEnum {
|
||||
|
||||
/**
|
||||
* 正确执行后的返回
|
||||
*/
|
||||
OK("00000", "一切 ok"),
|
||||
|
||||
/**
|
||||
* 一级宏观错误码,用户端错误
|
||||
*/
|
||||
USER_ERROR("A0001", "用户端错误"),
|
||||
|
||||
/**
|
||||
* 二级宏观错误码,用户注册错误
|
||||
*/
|
||||
USER_REGISTER_ERROR("A0100", "用户注册错误"),
|
||||
|
||||
/**
|
||||
* 用户未同意隐私协议
|
||||
*/
|
||||
USER_NO_AGREE_PRIVATE_ERROR("A0101", "用户未同意隐私协议"),
|
||||
|
||||
/**
|
||||
* 注册国家或地区受限
|
||||
*/
|
||||
USER_REGISTER_AREA_LIMIT_ERROR("A0102", "注册国家或地区受限"),
|
||||
|
||||
/**
|
||||
* 用户验证码错误
|
||||
*/
|
||||
USER_VERIFY_CODE_ERROR("A0240", "用户验证码错误"),
|
||||
|
||||
/**
|
||||
* 用户名已存在
|
||||
*/
|
||||
USER_NAME_EXIST("A0111", "用户名已存在"),
|
||||
|
||||
/**
|
||||
* 用户账号不存在
|
||||
*/
|
||||
USER_ACCOUNT_NOT_EXIST("A0201", "用户账号不存在"),
|
||||
|
||||
/**
|
||||
* 用户密码错误
|
||||
*/
|
||||
USER_PASSWORD_ERROR("A0210", "用户密码错误"),
|
||||
|
||||
/**
|
||||
* 二级宏观错误码,用户请求参数错误
|
||||
*/
|
||||
USER_REQUEST_PARAM_ERROR("A0400", "用户请求参数错误"),
|
||||
|
||||
/**
|
||||
* 用户登录已过期
|
||||
*/
|
||||
USER_LOGIN_EXPIRED("A0230", "用户登录已过期"),
|
||||
|
||||
/**
|
||||
* 访问未授权
|
||||
*/
|
||||
USER_UN_AUTH("A0301", "访问未授权"),
|
||||
|
||||
/**
|
||||
* 用户请求服务异常
|
||||
*/
|
||||
USER_REQ_EXCEPTION("A0500", "用户请求服务异常"),
|
||||
|
||||
/**
|
||||
* 请求超出限制
|
||||
*/
|
||||
USER_REQ_MANY("A0501", "请求超出限制"),
|
||||
|
||||
/**
|
||||
* 用户评论异常
|
||||
*/
|
||||
USER_COMMENT("A2000", "用户评论异常"),
|
||||
|
||||
/**
|
||||
* 用户评论异常
|
||||
*/
|
||||
USER_COMMENTED("A2001", "用户已发表评论"),
|
||||
|
||||
/**
|
||||
* 作家发布异常
|
||||
*/
|
||||
AUTHOR_PUBLISH("A3000", "作家发布异常"),
|
||||
|
||||
/**
|
||||
* 小说名已存在
|
||||
*/
|
||||
AUTHOR_BOOK_NAME_EXIST("A3001", "小说名已存在"),
|
||||
|
||||
/**
|
||||
* 用户上传文件异常
|
||||
*/
|
||||
USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"),
|
||||
|
||||
/**
|
||||
* 用户上传文件类型不匹配
|
||||
*/
|
||||
USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"),
|
||||
|
||||
/**
|
||||
* 一级宏观错误码,系统执行出错
|
||||
*/
|
||||
SYSTEM_ERROR("B0001", "系统执行出错"),
|
||||
|
||||
/**
|
||||
* 二级宏观错误码,系统执行超时
|
||||
*/
|
||||
SYSTEM_TIMEOUT_ERROR("B0100", "系统执行超时"),
|
||||
|
||||
/**
|
||||
* 一级宏观错误码,调用第三方服务出错
|
||||
*/
|
||||
THIRD_SERVICE_ERROR("C0001", "调用第三方服务出错"),
|
||||
|
||||
/**
|
||||
* 一级宏观错误码,中间件服务出错
|
||||
*/
|
||||
MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错");
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* 中文描述
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package io.github.xxyopen.novel.common.constant;
|
||||
|
||||
/**
|
||||
* 消息发送器的类型
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2023/3/24
|
||||
*/
|
||||
public class MessageSenderTypeConsts {
|
||||
|
||||
private MessageSenderTypeConsts() {
|
||||
throw new IllegalStateException("Constant class");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册成功的邮件发送器
|
||||
*/
|
||||
public static final String REGISTER_MAIL_SENDER = "registerMailSender";
|
||||
|
||||
/**
|
||||
* 秒杀活动的系统通知发送器
|
||||
*/
|
||||
public static final String SECKILL_SYS_NOTICE_SENDER = "seckillSysNoticeSender";
|
||||
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package io.github.xxyopen.novel.common.constant;
|
||||
|
||||
/**
|
||||
* 系统配置相关常量
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/12
|
||||
*/
|
||||
public class SystemConfigConsts {
|
||||
|
||||
private SystemConfigConsts() {
|
||||
throw new IllegalStateException(CONST_INSTANCE_EXCEPTION_MSG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Http 请求认证 Header
|
||||
*/
|
||||
public static final String HTTP_AUTH_HEADER_NAME = "Authorization";
|
||||
|
||||
/**
|
||||
* 前台门户系统标识
|
||||
*/
|
||||
public static final String NOVEL_FRONT_KEY = "front";
|
||||
|
||||
/**
|
||||
* 作家管理系统标识
|
||||
*/
|
||||
public static final String NOVEL_AUTHOR_KEY = "author";
|
||||
|
||||
/**
|
||||
* 后台管理系统标识
|
||||
*/
|
||||
public static final String NOVEL_ADMIN_KEY = "admin";
|
||||
|
||||
/**
|
||||
* 图片上传目录
|
||||
*/
|
||||
public static final String IMAGE_UPLOAD_DIRECTORY = "/image/";
|
||||
|
||||
/**
|
||||
* 常量类实例化异常信息
|
||||
*/
|
||||
public static final String CONST_INSTANCE_EXCEPTION_MSG = "Constant class";
|
||||
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package io.github.xxyopen.novel.common.json.deserializer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import java.io.IOException;
|
||||
import org.springframework.boot.jackson.JsonComponent;
|
||||
|
||||
|
||||
/**
|
||||
* JSON 全局反序列化器
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/21
|
||||
*/
|
||||
@JsonComponent
|
||||
public class GlobalJsonDeserializer {
|
||||
|
||||
/**
|
||||
* 字符串反序列化器:过滤特殊字符,解决 XSS 攻击
|
||||
*/
|
||||
public static class StringDeserializer extends JsonDeserializer<String> {
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext) throws IOException {
|
||||
return jsonParser.getValueAsString()
|
||||
.replace("<", "<")
|
||||
.replace(">", ">");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package io.github.xxyopen.novel.common.json.serializer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 用户名序列化器(敏感信息,不应该在页面上完全显示)
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/20
|
||||
*/
|
||||
public class UsernameSerializer extends JsonSerializer<String> {
|
||||
|
||||
@Override
|
||||
public void serialize(String s, JsonGenerator jsonGenerator,
|
||||
SerializerProvider serializerProvider) throws IOException {
|
||||
jsonGenerator.writeString(s.substring(0, 4) + "****" + s.substring(8));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package io.github.xxyopen.novel.common.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分页请求数据格式封装,所有分页请求的Dto类都应继承该类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/11
|
||||
*/
|
||||
@Data
|
||||
public class PageReqDto {
|
||||
|
||||
/**
|
||||
* 请求页码,默认第 1 页
|
||||
*/
|
||||
@Parameter(description = "请求页码,默认第 1 页")
|
||||
private int pageNum = 1;
|
||||
|
||||
/**
|
||||
* 每页大小,默认每页 10 条
|
||||
*/
|
||||
@Parameter(description = "每页大小,默认每页 10 条")
|
||||
private int pageSize = 10;
|
||||
|
||||
/**
|
||||
* 是否查询所有,默认不查所有 为 true 时,pageNum 和 pageSize 无效
|
||||
*/
|
||||
@Parameter(hidden = true)
|
||||
private boolean fetchAll = false;
|
||||
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package io.github.xxyopen.novel.common.resp;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 分页响应数据格式封装
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/11
|
||||
*/
|
||||
@Getter
|
||||
public class PageRespDto<T> {
|
||||
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
private final long pageNum;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
private final long pageSize;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private final long total;
|
||||
|
||||
/**
|
||||
* 分页数据集
|
||||
*/
|
||||
private final List<? extends T> list;
|
||||
|
||||
/**
|
||||
* 该构造函数用于通用分页查询的场景 接收普通分页数据和普通集合
|
||||
*/
|
||||
public PageRespDto(long pageNum, long pageSize, long total, List<T> list) {
|
||||
this.pageNum = pageNum;
|
||||
this.pageSize = pageSize;
|
||||
this.total = total;
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public static <T> PageRespDto<T> of(long pageNum, long pageSize, long total, List<T> list) {
|
||||
return new PageRespDto<>(pageNum, pageSize, total, list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分页数
|
||||
*/
|
||||
public long getPages() {
|
||||
if (this.pageSize == 0L) {
|
||||
return 0L;
|
||||
} else {
|
||||
long pages = this.total / this.pageSize;
|
||||
if (this.total % this.pageSize != 0L) {
|
||||
++pages;
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
package io.github.xxyopen.novel.common.resp;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Http Rest 响应工具及数据格式封装
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/11
|
||||
*/
|
||||
@Getter
|
||||
public class RestResp<T> {
|
||||
|
||||
/**
|
||||
* 响应码
|
||||
*/
|
||||
@Schema(description = "错误码,00000-没有错误")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
@Schema(description = "响应消息")
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
@Schema(description = "响应数据")
|
||||
private T data;
|
||||
|
||||
private RestResp() {
|
||||
this.code = ErrorCodeEnum.OK.getCode();
|
||||
this.message = ErrorCodeEnum.OK.getMessage();
|
||||
}
|
||||
|
||||
private RestResp(ErrorCodeEnum errorCode) {
|
||||
this.code = errorCode.getCode();
|
||||
this.message = errorCode.getMessage();
|
||||
}
|
||||
|
||||
private RestResp(T data) {
|
||||
this();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务处理成功,无数据返回
|
||||
*/
|
||||
public static RestResp<Void> ok() {
|
||||
return new RestResp<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务处理成功,有数据返回
|
||||
*/
|
||||
public static <T> RestResp<T> ok(T data) {
|
||||
return new RestResp<>(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务处理失败
|
||||
*/
|
||||
public static RestResp<Void> fail(ErrorCodeEnum errorCode) {
|
||||
return new RestResp<>(errorCode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 系统错误
|
||||
*/
|
||||
public static RestResp<Void> error() {
|
||||
return new RestResp<>(ErrorCodeEnum.SYSTEM_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否成功
|
||||
*/
|
||||
public boolean isOk() {
|
||||
return Objects.equals(this.code, ErrorCodeEnum.OK.getCode());
|
||||
}
|
||||
|
||||
}
|
159
novel-core/novel-config/pom.xml
Normal file
159
novel-core/novel-config/pom.xml
Normal file
@@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>novel-core</artifactId>
|
||||
<groupId>io.github.xxyopen</groupId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>novel-config</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<!-- Exclude the Tomcat dependency -->
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Use Undertow instead -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- mybatis-plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 缓存相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 请求参数校验相关 -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MQ 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ShardingSphere-JDBC -->
|
||||
<dependency>
|
||||
<groupId>org.apache.shardingsphere</groupId>
|
||||
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
|
||||
<version>${shardingsphere-jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot 管理和监控 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redisson 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>${redisson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Aop 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenAPI 3 -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc-openapi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.xxyopen</groupId>
|
||||
<artifactId>novel-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,44 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.AmqpConsts;
|
||||
import org.springframework.amqp.core.Binding;
|
||||
import org.springframework.amqp.core.BindingBuilder;
|
||||
import org.springframework.amqp.core.FanoutExchange;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* AMQP 配置类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/25
|
||||
*/
|
||||
@Configuration
|
||||
public class AmqpConfig {
|
||||
|
||||
/**
|
||||
* 小说信息改变交换机
|
||||
*/
|
||||
@Bean
|
||||
public FanoutExchange bookChangeExchange() {
|
||||
return new FanoutExchange(AmqpConsts.BookChangeMq.EXCHANGE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch book 索引更新队列
|
||||
*/
|
||||
@Bean
|
||||
public Queue esBookUpdateQueue() {
|
||||
return new Queue(AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch book 索引更新队列绑定到小说信息改变交换机
|
||||
*/
|
||||
@Bean
|
||||
public Binding esBookUpdateQueueBinding() {
|
||||
return BindingBuilder.bind(esBookUpdateQueue()).to(bookChangeExchange());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import io.github.xxyopen.novel.common.constant.CacheConsts;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.caffeine.CaffeineCache;
|
||||
import org.springframework.cache.support.SimpleCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.cache.RedisCacheWriter;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
|
||||
/**
|
||||
* 缓存配置类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/12
|
||||
*/
|
||||
@Configuration
|
||||
public class CacheConfig {
|
||||
|
||||
/**
|
||||
* Caffeine 缓存管理器
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public CacheManager caffeineCacheManager() {
|
||||
SimpleCacheManager cacheManager = new SimpleCacheManager();
|
||||
|
||||
List<CaffeineCache> caches = new ArrayList<>(CacheConsts.CacheEnum.values().length);
|
||||
// 类型推断 var 非常适合 for 循环,JDK 10 引入,JDK 11 改进
|
||||
for (var c : CacheConsts.CacheEnum.values()) {
|
||||
if (c.isLocal()) {
|
||||
Caffeine<Object, Object> caffeine = Caffeine.newBuilder().recordStats()
|
||||
.maximumSize(c.getMaxSize());
|
||||
if (c.getTtl() > 0) {
|
||||
caffeine.expireAfterWrite(Duration.ofSeconds(c.getTtl()));
|
||||
}
|
||||
caches.add(new CaffeineCache(c.getName(), caffeine.build()));
|
||||
}
|
||||
}
|
||||
|
||||
cacheManager.setCaches(caches);
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 缓存管理器
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
|
||||
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(
|
||||
connectionFactory);
|
||||
|
||||
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
|
||||
.disableCachingNullValues().prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX);
|
||||
|
||||
Map<String, RedisCacheConfiguration> cacheMap = new LinkedHashMap<>(
|
||||
CacheConsts.CacheEnum.values().length);
|
||||
// 类型推断 var 非常适合 for 循环,JDK 10 引入,JDK 11 改进
|
||||
for (var c : CacheConsts.CacheEnum.values()) {
|
||||
if (c.isRemote()) {
|
||||
if (c.getTtl() > 0) {
|
||||
cacheMap.put(c.getName(),
|
||||
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
|
||||
.prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX)
|
||||
.entryTtl(Duration.ofSeconds(c.getTtl())));
|
||||
} else {
|
||||
cacheMap.put(c.getName(),
|
||||
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
|
||||
.prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
|
||||
defaultCacheConfig, cacheMap);
|
||||
redisCacheManager.setTransactionAware(true);
|
||||
redisCacheManager.initializeCaches();
|
||||
return redisCacheManager;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Mybatis-Plus 配置类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/16
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 分页插件
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.SystemConfigConsts;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.info.License;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
/**
|
||||
* OpenApi 配置类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/9/1
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("dev")
|
||||
@OpenAPIDefinition(info = @Info(title = "novel 项目接口文档", version = "v3.2.0", license = @License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0")))
|
||||
@SecurityScheme(type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME, description = "登录 token")
|
||||
public class OpenApiConfig {
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Xss 过滤配置属性
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/17
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "novel.xss")
|
||||
public record XssProperties(Boolean enabled, List<String> excludes) {
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package io.github.xxyopen.novel.config.annotation;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 分布式锁-Key 注解
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/6/20
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
|
||||
public @interface Key {
|
||||
|
||||
String expr() default "";
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package io.github.xxyopen.novel.config.annotation;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* 分布式锁 注解
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/6/20
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target(METHOD)
|
||||
public @interface Lock {
|
||||
|
||||
String prefix();
|
||||
|
||||
boolean isWait() default false;
|
||||
|
||||
long waitTime() default 3L;
|
||||
|
||||
ErrorCodeEnum failCode() default ErrorCodeEnum.OK;
|
||||
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
package io.github.xxyopen.novel.config.aspect;
|
||||
|
||||
import io.github.xxyopen.novel.config.annotation.Key;
|
||||
import io.github.xxyopen.novel.config.annotation.Lock;
|
||||
import io.github.xxyopen.novel.config.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.common.TemplateParserContext;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 分布式锁 切面
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/6/20
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LockAspect {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
private static final String KEY_PREFIX = "Lock";
|
||||
|
||||
private static final String KEY_SEPARATOR = "::";
|
||||
|
||||
@Around(value = "@annotation(io.github.xxyopen.novel.config.annotation.Lock)")
|
||||
@SneakyThrows
|
||||
public Object doAround(ProceedingJoinPoint joinPoint) {
|
||||
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
|
||||
Method targetMethod = methodSignature.getMethod();
|
||||
Lock lock = targetMethod.getAnnotation(Lock.class);
|
||||
String lockKey = KEY_PREFIX + buildLockKey(lock.prefix(), targetMethod,
|
||||
joinPoint.getArgs());
|
||||
RLock rLock = redissonClient.getLock(lockKey);
|
||||
if (lock.isWait() ? rLock.tryLock(lock.waitTime(), TimeUnit.SECONDS) : rLock.tryLock()) {
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
rLock.unlock();
|
||||
}
|
||||
}
|
||||
throw new BusinessException(lock.failCode());
|
||||
}
|
||||
|
||||
private String buildLockKey(String prefix, Method method, Object[] args) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (StringUtils.hasText(prefix)) {
|
||||
builder.append(KEY_SEPARATOR).append(prefix);
|
||||
}
|
||||
Parameter[] parameters = method.getParameters();
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
builder.append(KEY_SEPARATOR);
|
||||
if (parameters[i].isAnnotationPresent(Key.class)) {
|
||||
Key key = parameters[i].getAnnotation(Key.class);
|
||||
builder.append(parseKeyExpr(key.expr(), args[i]));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String parseKeyExpr(String expr, Object arg) {
|
||||
if (!StringUtils.hasText(expr)) {
|
||||
return arg.toString();
|
||||
}
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expression = parser.parseExpression(expr, new TemplateParserContext());
|
||||
return expression.getValue(arg, String.class);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package io.github.xxyopen.novel.config.exception;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 自定义业务异常,用于处理用户请求时,业务错误时抛出
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/11
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final ErrorCodeEnum errorCodeEnum;
|
||||
|
||||
public BusinessException(ErrorCodeEnum errorCodeEnum) {
|
||||
// 不调用父类 Throwable的fillInStackTrace() 方法生成栈追踪信息,提高应用性能
|
||||
// 构造器之间的调用必须在第一行
|
||||
super(errorCodeEnum.getMessage(), null, false, false);
|
||||
this.errorCodeEnum = errorCodeEnum;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
package io.github.xxyopen.novel.config.exception;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
|
||||
import io.github.xxyopen.novel.common.resp.RestResp;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 通用的异常处理器
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/11
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class CommonExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理数据校验异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public RestResp<Void> handlerBindException(BindException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public RestResp<Void> handlerBusinessException(BusinessException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return RestResp.fail(e.getErrorCodeEnum());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public RestResp<Void> handlerException(Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return RestResp.error();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
package io.github.xxyopen.novel.config.filter;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import io.github.xxyopen.novel.config.XssProperties;
|
||||
import io.github.xxyopen.novel.config.wrapper.XssHttpServletRequestWrapper;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 防止 XSS 攻击的过滤器
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/17
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "novel.xss.enabled", havingValue = "true")
|
||||
@WebFilter(urlPatterns = "/*", filterName = "xssFilter")
|
||||
@EnableConfigurationProperties(value = {XssProperties.class})
|
||||
@RequiredArgsConstructor
|
||||
public class XssFilter implements Filter {
|
||||
|
||||
private final XssProperties xssProperties;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
Filter.super.init(filterConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
|
||||
FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) servletRequest;
|
||||
if (handleExcludeUrl(req)) {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
return;
|
||||
}
|
||||
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(
|
||||
(HttpServletRequest) servletRequest);
|
||||
filterChain.doFilter(xssRequest, servletResponse);
|
||||
}
|
||||
|
||||
private boolean handleExcludeUrl(HttpServletRequest request) {
|
||||
if (CollectionUtils.isEmpty(xssProperties.excludes())) {
|
||||
return false;
|
||||
}
|
||||
String url = request.getServletPath();
|
||||
for (String pattern : xssProperties.excludes()) {
|
||||
Pattern p = Pattern.compile("^" + pattern);
|
||||
Matcher m = p.matcher(url);
|
||||
if (m.find()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
Filter.super.destroy();
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package io.github.xxyopen.novel.config.interceptor;
|
||||
|
||||
import io.github.xxyopen.novel.common.auth.JwtUtils;
|
||||
import io.github.xxyopen.novel.common.auth.UserHolder;
|
||||
import io.github.xxyopen.novel.common.constant.SystemConfigConsts;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* Token 解析拦截器
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/27
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TokenParseInterceptor implements HandlerInterceptor {
|
||||
|
||||
@SuppressWarnings("NullableProblems")
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
// 获取登录 JWT
|
||||
String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);
|
||||
if (StringUtils.hasText(token)) {
|
||||
// 解析 token 并保存
|
||||
UserHolder.setUserId(JwtUtils.parseToken(token, SystemConfigConsts.NOVEL_FRONT_KEY));
|
||||
}
|
||||
return HandlerInterceptor.super.preHandle(request, response, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* DispatcherServlet 完全处理完请求后调用,出现异常照常调用
|
||||
*/
|
||||
@SuppressWarnings("NullableProblems")
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
|
||||
throws Exception {
|
||||
// 清理当前线程保存的用户数据
|
||||
UserHolder.clear();
|
||||
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package io.github.xxyopen.novel.config.wrapper;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XSS 过滤处理
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/17
|
||||
*/
|
||||
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private static final Map<String, String> REPLACE_RULE = new HashMap<>();
|
||||
|
||||
static {
|
||||
REPLACE_RULE.put("<", "<");
|
||||
REPLACE_RULE.put(">", ">");
|
||||
}
|
||||
|
||||
public XssHttpServletRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (values != null) {
|
||||
int length = values.length;
|
||||
String[] escapeValues = new String[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
escapeValues[i] = values[i];
|
||||
int index = i;
|
||||
REPLACE_RULE.forEach(
|
||||
(k, v) -> escapeValues[index] = escapeValues[index].replaceAll(k, v));
|
||||
}
|
||||
return escapeValues;
|
||||
}
|
||||
return new String[0];
|
||||
}
|
||||
}
|
@@ -0,0 +1,142 @@
|
||||
#--------------------- Spring Cloud 配置-------------------
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 192.168.10.110:8848
|
||||
openfeign:
|
||||
lazy-attributes-resolution: true
|
||||
|
||||
feign:
|
||||
sentinel:
|
||||
enabled: true
|
||||
|
||||
--- #--------------------------通用配置-------------------------
|
||||
spring:
|
||||
jackson:
|
||||
generator:
|
||||
# JSON 序列化时,将所有 Number 类型的属性都转为 String 类型返回,避免前端数据精度丢失的问题。
|
||||
# 由于 Javascript 标准规定所有数字处理都应使用 64 位 IEEE 754 浮点值完成,
|
||||
# 结果是某些 64 位整数值无法准确表示(尾数只有 51 位宽)
|
||||
write-numbers-as-strings: true
|
||||
|
||||
--- #---------------------数据库配置---------------------------
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: test123456
|
||||
# ShardingSphere-JDBC 配置
|
||||
# 配置是 ShardingSphere-JDBC 中唯一与应用开发者交互的模块,
|
||||
# 通过它可以快速清晰的理解 ShardingSphere-JDBC 所提供的功能。
|
||||
shardingsphere:
|
||||
# 是否开启分库分表
|
||||
enabled: false
|
||||
props:
|
||||
# 是否在日志中打印 SQL
|
||||
sql-show: true
|
||||
# 模式配置
|
||||
mode:
|
||||
# 单机模式
|
||||
type: Standalone
|
||||
repository:
|
||||
# 文件持久化
|
||||
type: File
|
||||
props:
|
||||
# 元数据存储路径
|
||||
path: .shardingsphere
|
||||
# 使用本地配置覆盖持久化配置
|
||||
overwrite: true
|
||||
# 数据源配置
|
||||
datasource:
|
||||
names: ds_0
|
||||
ds_0:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
jdbcUrl: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: test123456
|
||||
# 规则配置
|
||||
rules:
|
||||
# 数据分片
|
||||
sharding:
|
||||
tables:
|
||||
# book_content 表
|
||||
book_content:
|
||||
# 数据节点
|
||||
actual-data-nodes: ds_$->{0}.book_content$->{0..9}
|
||||
# 分表策略
|
||||
table-strategy:
|
||||
standard:
|
||||
# 分片列名称
|
||||
sharding-column: chapter_id
|
||||
# 分片算法名称
|
||||
sharding-algorithm-name: bookContentSharding
|
||||
sharding-algorithms:
|
||||
bookContentSharding:
|
||||
# 行表达式分片算法,使用 Groovy 的表达式,提供对 SQL 语句中的 = 和 IN 的分片操作支持
|
||||
type: INLINE
|
||||
props:
|
||||
# 分片算法的行表达式
|
||||
algorithm-expression: book_content$->{chapter_id % 10}
|
||||
|
||||
--- #---------------------中间件配置---------------------------
|
||||
spring:
|
||||
data:
|
||||
# Redis 配置
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
# password: 123456
|
||||
|
||||
# RabbitMQ 配置
|
||||
rabbitmq:
|
||||
addresses: "amqp://xxyopen:test123456@192.168.10.110"
|
||||
virtual-host: novel
|
||||
template:
|
||||
retry:
|
||||
# 开启重试
|
||||
enabled: true
|
||||
# 最大重试次数
|
||||
max-attempts: 3
|
||||
# 第一次和第二次重试之间的持续时间
|
||||
initial-interval: "3s"
|
||||
|
||||
--- #----------------------安全配置---------------------------
|
||||
# Actuator 端点管理
|
||||
management:
|
||||
# 端点公开配置
|
||||
endpoints:
|
||||
# 通过 HTTP 公开的 Web 端点
|
||||
web:
|
||||
exposure:
|
||||
# 公开所有的 Web 端点
|
||||
include: "*"
|
||||
info:
|
||||
env:
|
||||
# 公开所有以 info. 开头的环境属性
|
||||
enabled: true
|
||||
health:
|
||||
rabbit:
|
||||
# 关闭 rabbitmq 的健康检查
|
||||
enabled: true
|
||||
elasticsearch:
|
||||
# 关闭 elasticsearch 的健康检查
|
||||
enabled: true
|
||||
|
||||
--- #---------------------自定义配置----------------------------
|
||||
novel:
|
||||
# XSS 过滤配置
|
||||
xss:
|
||||
# 过滤开关
|
||||
enabled: true
|
||||
# 排除链接
|
||||
excludes:
|
||||
- /system/notice/*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,15 @@
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
config:
|
||||
server-addr: 192.168.10.110:8848
|
||||
file-extension: yml
|
||||
extension-configs[0]:
|
||||
data‐id: novel-mysql.yml
|
||||
refresh: true
|
||||
extension-configs[1]:
|
||||
data‐id: novel-redis.yml
|
||||
refresh: true
|
||||
extension-configs[2]:
|
||||
data‐id: novel-rabbitmq.yml
|
||||
refresh: true
|
19
novel-core/pom.xml
Normal file
19
novel-core/pom.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modules>
|
||||
<module>novel-common</module>
|
||||
<module>novel-config</module>
|
||||
</modules>
|
||||
<parent>
|
||||
<artifactId>novel-cloud</artifactId>
|
||||
<groupId>io.github.xxyopen</groupId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>novel-core</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
</project>
|
Reference in New Issue
Block a user