Compare commits

..

No commits in common. "master" and "v3.0.0" have entirely different histories.

132 changed files with 924 additions and 4995 deletions

1
.gitignore vendored
View File

@ -33,4 +33,3 @@ build/
.vscode/ .vscode/
/logs/ /logs/
/.shardingsphere/

194
README.md
View File

@ -1,76 +1,57 @@
[![index]( https://youdoc.github.io/img/tencent.jpg )]( https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=736e609d66e0ac4e57813316cec6fd0b&from=console ) [![index]( https://s1.ax1x.com/2022/05/17/O5tgbR.png )]( https://curl.qcloud.com/kgMaOjoq )
<p align="center">
<a href='https://github.com/201206030/novel'><img alt="Github stars" src="https://img.shields.io/github/stars/201206030/novel?logo=github"></a>
<a href='https://github.com/201206030/novel'><img alt="Github forks" src="https://img.shields.io/github/forks/201206030/novel?logo=github"></a>
<a href='https://gitee.com/novel_dev_team/novel'><img alt="Gitee stars" src="https://gitee.com/novel_dev_team/novel/badge/star.svg?theme=gitee"></a>
<a href='https://gitee.com/novel_dev_team/novel'><img alt="Gitee forks" src="https://gitee.com/novel_dev_team/novel/badge/fork.svg?theme=gitee"></a>
</p>
[![Github stars](https://img.shields.io/github/stars/201206030/novel?logo=github)](https://github.com/201206030/novel)
[![Github forks](https://img.shields.io/github/forks/201206030/novel?logo=github)](https://github.com/201206030/novel)
[![Gitee star](https://gitee.com/novel_dev_team/novel/badge/star.svg?theme=gitee)](https://gitee.com/novel_dev_team/novel)
[![Gitee fork](https://gitee.com/novel_dev_team/novel/badge/fork.svg?theme=gitee)](https://gitee.com/novel_dev_team/novel)
## 项目简介 ## 项目简介
novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开发的前后端分离**学习型** novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开发的前后端分离的**学习型**小说项目,配备详细的项目教程手把手教你**从零开始**开发上线一个生产级别的 Java 系统,由小说门户系统、作家后台管理系统、平台后台管理系统、爬虫管理系统等多个子系统构成。包括小说推荐、作品检索、小说排行榜、小说阅读、小说评论、充值订阅、新闻发布等功能。
小说项目,配备[保姆级教程](https://docs.xxyopen.com/course/novel)手把手教你**从零开始**开发上线一套生产级别的 Java
系统,由小说门户系统、作家后台管理系统、平台后台管理系统等多个子系统构成。包括小说推荐、作品检索、小说排行榜、小说阅读、小说评论、会员中心、作家专区、充值订阅、新闻发布等功能。
## 项目地址 ## 项目地址
- 后端项目(更新中):[GitHub](https://github.com/201206030/novel) [码云](https://gitee.com/novel_dev_team/novel) - 后端项目(更新中):[GitHub](https://github.com/201206030/novel) [码云](https://gitee.com/novel_dev_team/novel)
- 前端项目(更新中):[GitHub](https://github.com/201206030/novel-front-web) - 后端微服务版本项目(待更新):[GitHub](https://github.com/201206030/novel-cloud) [码云](https://gitee.com/novel_dev_team/novel-cloud)
[码云](https://gitee.com/novel_dev_team/novel-front-web) - 前端小说门户系统项目(更新中):[GitHub](https://github.com/201206030/novel-front-web) [码云](https://gitee.com/novel_dev_team/novel-front-web)
- 线上应用版:[GitHub](https://github.com/201206030/novel-plus) [码云](https://gitee.com/novel_dev_team/novel-plus) - 前端作家管理系统项目:待上线
- 微服务版:[GitHub](https://github.com/201206030/novel-cloud) [码云](https://gitee.com/novel_dev_team/novel-cloud) - 前端平台后台管理系统项目:待上线
- 线上应用版:[GitHub](https://github.com/201206030/novel-plus) [码云](https://gitee.com/novel_dev_team/novel-plus) [演示地址](http://47.106.243.172:8888/)
## 开发环境 ## 开发环境
- MySQL 8.0 - MySQL 8.0
- Redis 7.0 - Redis 7.0
- Elasticsearch 8.2.0(可选) - JDK 17
- RabbitMQ 3.10.2(可选)
- XXL-JOB 2.3.1(可选)
- JDK 21
- Maven 3.8 - Maven 3.8
- IntelliJ IDEA可选 - IntelliJ IDEA 2021.3(可选)
- Node 16.14 - Node 16.14
**注Elasticsearch、RabbitMQ 和 XXL-JOB 默认关闭,可通过 application.yml 配置文件中相应的`enable`配置属性开启。**
## 后端技术选型 ## 后端技术选型
| 技术 | 版本 | 说明 | 官网 | 学习 | | 技术 | 版本 | 说明 | 官网 | 学习 |
|---------------------|:------------:|-------------------------| ------------------------------------ |:------------------------------------------------------------------------------------------------------------------------:| | :----------------- | :-----: | -------------------------- | --------------------------------------- | :-------------------------------------------------: |
| Spring Boot | 3.3.0 | 容器 + MVC 框架 | [进入](https://spring.io/projects/spring-boot) | [进入](https://docs.spring.io/spring-boot/docs/3.0.0/reference/html) | | Spring Boot | 3.0.0-SNAPSHOT | 容器 + MVC 框架 | https://spring.io/projects/spring-boot | [进入](https://youdoc.github.io/course/novel/11.html) |
| Spring AI | 1.0.0-M6 | Spring 官方 AI 框架 | [进入](https://spring.io/projects/spring-ai) | [进入](https://docs.spring.io/spring-ai/reference/) | | Mybatis | 3.5.9 | ORM 框架 | http://www.mybatis.org | [进入](https://mybatis.org/mybatis-3/zh/index.html) |
| MyBatis | 3.5.9 | ORM 框架 | [进入](http://www.mybatis.org) | [进入](https://mybatis.org/mybatis-3/zh/index.html) | | MyBatis-Plus | 3.5.1 | Mybatis 增强工具 | https://baomidou.com/ | [进入](https://baomidou.com/pages/24112f/) |
| MyBatis-Plus | 3.5.3 | MyBatis 增强工具 | [进入](https://baomidou.com/) | [进入](https://baomidou.com/pages/24112f/) | | JJWT | 0.11.5 | JWT 登录支持 | https://github.com/jwtk/jjwt | - |
| JJWT | 0.11.5 | JWT 登录支持 | [进入](https://github.com/jwtk/jjwt) | - | | Lombok | 1.18.24 | 简化对象封装工具 | https://github.com/projectlombok/lombok | [进入](https://projectlombok.org/features/all) |
| Lombok | 1.18.24 | 简化对象封装工具 | [进入](https://github.com/projectlombok/lombok) | [进入](https://projectlombok.org/features/all) | | Caffeine | 3.1.0 | 本地缓存支持 | https://github.com/ben-manes/caffeine | [进入](https://github.com/ben-manes/caffeine/wiki/Home-zh-CN) |
| Caffeine | 3.1.0 | 本地缓存支持 | [进入](https://github.com/ben-manes/caffeine) | [进入](https://github.com/ben-manes/caffeine/wiki/Home-zh-CN) | | Redis | 7.0 | 分布式缓存支持 | https://redis.io | [进入](https://redis.io/docs) |
| Redis | 7.0 | 分布式缓存支持 | [进入](https://redis.io) | [进入](https://redis.io/docs) | | MySQL | 8.0 | 数据库服务 | https://www.mysql.com | [进入](https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html) |
| Redisson | 3.17.4 | 分布式锁实现 | [进入](https://github.com/redisson/redisson) | [进入](https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95) | | Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 | https://undertow.io | [进入](https://undertow.io/documentation.html) |
| MySQL | 8.0 | 数据库服务 | [进入](https://www.mysql.com) | [进入](https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html) | | Docker | - | 应用容器引擎 | https://www.docker.com/ | - |
| ShardingSphere-JDBC | 5.5.1 | 数据库分库分表支持 | [进入](https://shardingsphere.apache.org) | [进入](https://shardingsphere.apache.org/document/5.1.1/cn/overview) | | Jenkins | - | 自动化部署工具 | https://github.com/jenkinsci/jenkins | - |
| Elasticsearch | 8.2.0 | 搜索引擎服务 | [进入](https://www.elastic.co) | [进入](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) | | Sonarqube | - | 代码质量控制 | https://www.sonarqube.org/ | - |
| RabbitMQ | 3.10.2 | 开源消息中间件 | [进入](https://www.rabbitmq.com) | [进入](https://www.rabbitmq.com/tutorials/tutorial-one-java.html) |
| XXL-JOB | 2.3.1 | 分布式任务调度平台 | [进入](https://www.xuxueli.com/xxl-job) | [进入](https://www.xuxueli.com/xxl-job) |
| Sentinel | 1.8.4 | 流量控制组件 | [进入](https://github.com/alibaba/Sentinel) | [进入](https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5) |
| Springdoc-openapi | 2.0.0 | Swagger 3 接口文档自动生成 | [进入](https://github.com/springdoc/springdoc-openapi) | [进入](https://springdoc.org/) |
| Spring Boot Admin | 3.0.0-M1 | 应用管理和监控 | [进入](https://github.com/codecentric/spring-boot-admin) | [进入](https://codecentric.github.io/spring-boot-admin/3.0.0-M1) |
| Tomcat | 10.1.24 | Spring Boot 默认内嵌 Web 容器 | [进入](https://tomcat.apache.org) | [进入](https://tomcat.apache.org/tomcat-10.1-doc/index.html) |
| Docker | - | 应用容器引擎 | [进入](https://www.docker.com/) | - |
| Jenkins | - | 自动化部署工具 | [进入](https://github.com/jenkinsci/jenkins) | - |
| Sonarqube | - | 代码质量控制 | [进入](https://www.sonarqube.org/) | - |
**注:更多热门新技术待集成。**
<!--| SpringFox Swagger2 | ?(不支持 Spring 6 | Spring项目接口文档生成工具 | https://github.com/springfox/springfox | - | -->
## 前端技术选型 ## 前端技术选型
| 技术 | 版本 | 说明 | 官网 | 学习 | | 技术 | 版本 | 说明 | 官网 | 学习 |
| :----------------- | :-----: | -------------------------- | --------------------------------------- | :-------------------------------------------------: | | :----------------- | :-----: | -------------------------- | --------------------------------------- | :-------------------------------------------------: |
| Vue.js | 3.2.13 | 渐进式 JavaScript 框架 | [进入](https://vuejs.org) | [进入](https://staging-cn.vuejs.org/guide/introduction.html) | | Vue.js | 3.2.13 | 渐进式 JavaScript 框架 | https://v3.cn.vuejs.org | [进入](https://v3.cn.vuejs.org/guide/introduction.html) |
| Vue Router | 4.0.15 | Vue.js 的官方路由 | [进入](https://router.vuejs.org) | [进入](https://router.vuejs.org/zh/guide/) | | Vue Router | 4.0.15 | Vue.js 的官方路由 | https://router.vuejs.org/zh/index.html | [进入](https://router.vuejs.org/zh/guide/) |
| axios | 0.27.2 | 基于 promise 的网络请求库 | [进入](https://axios-http.com) | [进入](https://axios-http.com/zh/docs/intro) | | axios | 0.27.2 | 基于 promise 的网络请求库 | https://axios-http.com/zh | [进入](https://axios-http.com/zh/docs/intro) |
| element-plus | 2.2.0 | 基于 Vue 3面向设计师和开发者的组件库 | [进入](https://element-plus.org) | [进入](https://element-plus.org/zh-CN/guide/design.html) | | element-plus | 2.2.0 | 基于 Vue 3面向设计师和开发者的组件库 | https://element-plus.org/zh-CN/ | [进入](https://element-plus.org/zh-CN/guide/design.html) |
## 编码规范 ## 编码规范
@ -99,14 +80,11 @@ io
| | +- resp -- 接口响应工具及响应数据格式封装 | | +- resp -- 接口响应工具及响应数据格式封装
| | +- util -- 通用工具 | | +- util -- 通用工具
| | | |
| +- annotation -- 自定义注解类
| +- aspect -- Spring AOP 切面
| +- auth -- 用户认证授权相关 | +- auth -- 用户认证授权相关
| +- config -- 业务相关配置 | +- config -- 业务相关配置
| +- constant -- 业务相关常量 | +- constant -- 业务相关常量
| +- filter -- 过滤器 | +- filter -- 过滤器
| +- interceptor -- 拦截器 | +- interceptor -- 拦截器
| +- json -- JSON 相关的包,包括序列化器和反序列化器
| +- task -- 定时任务 | +- task -- 定时任务
| +- util -- 业务相关工具 | +- util -- 业务相关工具
| +- wrapper -- 装饰器 | +- wrapper -- 装饰器
@ -181,52 +159,104 @@ io
![img](https://oscimg.oschina.net/oscnet/up-f849960f4c1303fea77d26e64fc505a7180.png) ![img](https://oscimg.oschina.net/oscnet/up-f849960f4c1303fea77d26e64fc505a7180.png)
11. 接口文档
![img](https://youdoc.github.io/img/novel/SwaggerUI.png)
## 安装步骤 ## 安装步骤
👉 [立即查看](https://docs.xxyopen.com/course/novel/#%E5%AE%89%E8%A3%85%E6%AD%A5%E9%AA%A4) 此安装步骤的前提是需要保证上一节的开发环境可用。
## 联系我们 - 下载后端源码
👉 [立即查看](https://novel.xxyopen.com/service.htm) ```
git clone https://gitee.com/novel_dev_team/novel.git
```
## 问题 - 数据库文件导入
### 为什么有 novel/novel-cloud 学习版? 1. 新建数据库(建议 novel
最开始是没有学习版的,只有一个爬虫/原创小说项目(最终发展成为 [novel-plus](https://github.com/201206030/novel-plus) 2. 解压后端源码`sql/novel.sql.zip`压缩包,得到数据库结构文件`novel_struc.sql`和数据库小说数据文件`novel_data.sql`
项目),用户群体大部分是对小说有兴趣,想自建一个干净无广告的小说网站的个人和站长。
后面随着使用人数逐渐增加,想通过这个项目来学习 Java 技术的人数也多了起来,对这部分用户来说,之前的项目用来学习很困难,具体原因如下: 3. 导入`novel_struct.sql`数据库结构文件
1. novel-plus 功能模块比较多,重复性的增删改查占了大部分,而用户时间是有限的,很难在有限的时间内筛选出对自己有帮助的功能模块来学习。 4. 导入`novel_data.sql`数据库小说数据文件
2. novel-plus 追求的是系统稳定,用户很难在其中学习到最新的技术。
3. novel-plus 代码规范性不够,受限于开发时间限制,代码开发时没有选择一个标准化的规范去参考。
4. novel-plus 文档缺失,由于功能比较多,整个系统的教程编写需要花费大量时间,即使教程最终上线成功,用户也不可能有那么多时间也没有意义去学习所有的功能。
最终novel单体架构 和 novel-cloud微服务架构诞生了这两个项目在保证核心流程完整的同时从 novel-plus - novel 后端服务安装
中选用了一些有代表性的功能,使用最新技术栈(不间断地更新和集成新技术),在[保姆级教程](https://docs.xxyopen.com/course/novel)的帮助下,尽量保证每一个功能都能让你学到不重复的技术。
所以这两个项目我的重点是去堆技术而不是去堆功能,功能只是其中的辅助,堆太多的重复性增删改查功能没有意义,对学习的帮助也不大。 1. 修改`src/resources/application.yml`配置文件中的数据源配置
### 谁适合使用 novel/novel-cloud 学习版项目? ```
spring:
datasource:
url: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: test123456
```
如果对下面任何一个问题你能回答 "是" 2. 修改`src/resources/application.yml`配置文件中的`redis`连接配置
1. 你没有项目经验,想学习如何从零开始开发上线一个生产级别的 Java 项目? ```
2. 你有项目经验,但是公司技术栈太落后,想学习最新的主流开发技术? spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
```
那么,本项目正是你需要的。 3. 项目根目录下运行如下命令来启动后端服务(有安装 IDE 的可以导入源码到 IDE 中运行)
### 谁暂时还不适合使用 novel/novel-cloud 学习版项目? ```
mvn spring-boot:run
```
如果对下面任何一个问题你能回答 "是" 4. 根据前后端的实际部署情况,修改`application.yml`中的跨域配置(默认情况可忽略此步骤)
1. 你不懂 Java - 下载前端前台门户系统源码
2. 你只是想搭建一个小说网站使用?
3. 你想找一个完整的 Java 商用项目,有时间也有耐心去学习项目中的方方面面?
那么,太遗憾了,本项目暂时不适合你,请使用 [novel-plus](https://github.com/201206030/novel-plus)。 ```
git clone https://gitee.com/novel_dev_team/novel-front-web.git
```
- novel-front-web 前端前台门户系统安装
1. 根据前后端的实际部署情况,修改`.env.development`中的`VUE_APP_BASE_API_URL`属性(默认情况可忽略此步骤)
2. `yarn`安装
```
npm install -g yarn
```
3. 项目根目录下运行如下命令来安装项目依赖
```
yarn install
```
4. 项目根目录下运行如下命令启动
```
yarn serve
```
5. 浏览器通过`http://localhost:1024`来访问
## 项目教程
[手把手教你从零开始开发上线一个生产级别的小说系统](https://youdoc.github.io/course/novel/3.html)
## 公众号
关注公众号接收项目最新动态,获取`Spring Boot 3`学习笔记!
加微信群交流,公众号后台回复「**微信群**」即可。
![微信公众号](https://youdoc.github.io/img/qrcode_for_gh.jpg)
## 赞赏支持
开源项目不易,若此项目能得到你的青睐,那么你可以赞赏支持作者持续开发与维护。
- 更完善的文档教程
- 服务器的费用也是一笔开销
- 为用户提供更好的开发环境
- 一杯咖啡
![mini-code](https://s1.ax1x.com/2020/10/31/BUQJwq.png)

View File

@ -1,62 +0,0 @@
PUT /book
{
"mappings" : {
"properties" : {
"id" : {
"type" : "long"
},
"authorId" : {
"type" : "long"
},
"authorName" : {
"type" : "text",
"analyzer": "ik_smart"
},
"bookName" : {
"type" : "text",
"analyzer": "ik_smart"
},
"bookDesc" : {
"type" : "text",
"analyzer": "ik_smart"
},
"bookStatus" : {
"type" : "short"
},
"categoryId" : {
"type" : "integer"
},
"categoryName" : {
"type" : "text",
"analyzer": "ik_smart"
},
"lastChapterId" : {
"type" : "long"
},
"lastChapterName" : {
"type" : "text",
"analyzer": "ik_smart"
},
"lastChapterUpdateTime" : {
"type": "long"
},
"picUrl" : {
"type" : "keyword",
"index" : false,
"doc_values" : false
},
"score" : {
"type" : "integer"
},
"wordCount" : {
"type" : "integer"
},
"workDirection" : {
"type" : "short"
},
"visitCount" : {
"type": "long"
}
}
}
}

View File

@ -1,4 +0,0 @@
1. 初始状态下MySQL 只需要执行 `novel.sql` 文件即可正常运行本系统
2. 只有开启 XXL-JOB 的功能,才需要执行 `xxl-job.sql` 文件
3. 只有开启 ShardingSphere-JDBC 的功能,才需要执行 `shardingsphere-jdbc.sql` 文件

View File

@ -1,171 +0,0 @@
DROP PROCEDURE IF EXISTS createBookChapterTable;
-- 创建小说章节表的存储过程
CREATE PROCEDURE createBookChapterTable()
BEGIN
-- 定义变量
DECLARE i int DEFAULT 0;
DECLARE tableName char(13) DEFAULT NULL;
while i < 10 do
set tableName = concat('book_chapter',i);
set @stmt = concat('create table ',tableName,'(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`book_id` bigint(20) unsigned NOT NULL COMMENT \'小说ID\',
`chapter_num` smallint(5) unsigned NOT NULL COMMENT \'章节号\',
`chapter_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT \'章节名\',
`word_count` int(10) unsigned NOT NULL COMMENT \'章节字数\',
`is_vip` tinyint(3) unsigned NOT NULL DEFAULT \'0\' COMMENT \'是否收费;1-收费 0-免费\',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_bookId_chapterNum` (`book_id`,`chapter_num`) USING BTREE,
UNIQUE KEY `pk_id` (`id`) USING BTREE,
KEY `idx_bookId` (`book_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT=\'小说章节\'');
prepare stmt from @stmt;
execute stmt;
deallocate prepare stmt;
set i = i + 1;
end while;
END;
call createBookChapterTable();
DROP PROCEDURE IF EXISTS createBookContentTable;
-- 创建小说内容表的存储过程
CREATE PROCEDURE createBookContentTable()
BEGIN
-- 定义变量
DECLARE i int DEFAULT 0;
DECLARE tableName char(13) DEFAULT NULL;
while i < 10 do
set tableName = concat('book_content',i);
set @stmt = concat('create table ',tableName,'(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT \'主键\',
`chapter_id` bigint(20) unsigned NOT NULL COMMENT \'章节ID\',
`content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT \'小说章节内容\',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_chapterId` (`chapter_id`) USING BTREE,
UNIQUE KEY `pk_id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT=\'小说内容\'');
prepare stmt from @stmt;
execute stmt;
deallocate prepare stmt;
set i = i + 1;
end while;
END;
call createBookContentTable();
DROP PROCEDURE IF EXISTS copyBookChapterData;
-- 迁移小说章节数据的存储过程
CREATE PROCEDURE copyBookChapterData()
BEGIN
-- 定义变量
DECLARE s int DEFAULT 0;
DECLARE chapterId bigint;
DECLARE bookId bigint;
DECLARE chapterNum smallint;
DECLARE chapterName varchar(100);
DECLARE wordCount int DEFAULT 0;
DECLARE isVip tinyint(64) DEFAULT 0;
DECLARE createTime datetime DEFAULT NULL;
DECLARE updateTime datetime DEFAULT NULL;
DECLARE tableNumber int DEFAULT 0;
DECLARE tableName char(13) DEFAULT NULL;
-- 定义游标
DECLARE report CURSOR FOR select id,book_id,chapter_num, chapter_name, word_count, is_vip,create_time,update_time from book_chapter;
-- 声明当游标遍历完后将标志变量置成某个值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET s=1;
-- 打开游标
open report;
-- 将游标中的值赋值给变量注意变量名不要和返回的列名同名变量顺序要和sql结果列的顺序一致
fetch report into chapterId,bookId,chapterNum, chapterName, wordCount,isVip,createTime,updateTime;
-- 循环遍历
while s<>1 do
-- 执行业务逻辑
set tableNumber = bookId % 10;
set tableName = concat('book_chapter',tableNumber);
set @stmt = concat('insert into ',tableName,'(`id`, `book_id`, `chapter_num`, `chapter_name`, `word_count`, `is_vip`, `create_time`, `update_time`) VALUES (',chapterId,', ',bookId,', ',chapterNum,', \'',chapterName,'\', ',wordCount,', ',isVip,', \'',createTime,'\', \'',updateTime,'\')');
prepare stmt from @stmt;
execute stmt;
deallocate prepare stmt;
fetch report into chapterId,bookId,chapterNum, chapterName, wordCount,isVip,createTime,updateTime;
end while;
-- 关闭游标
close report;
END;
call copyBookChapterData();
DROP PROCEDURE IF EXISTS copyBookContentData;
-- 迁移小说内容数据的存储过程
CREATE PROCEDURE copyBookContentData()
BEGIN
-- 定义变量
DECLARE s int DEFAULT 0;
DECLARE contentId bigint;
DECLARE chapterId bigint;
DECLARE bookContent mediumtext;
DECLARE createTime datetime DEFAULT NULL;
DECLARE updateTime datetime DEFAULT NULL;
DECLARE tableNumber int DEFAULT 0;
DECLARE tableName char(13) DEFAULT NULL;
-- 定义游标
DECLARE report CURSOR FOR select id,chapter_id,content,create_time,update_time from book_content;
-- 声明当游标遍历完后将标志变量置成某个值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET s=1;
-- 打开游标
open report;
-- 将游标中的值赋值给变量注意变量名不要和返回的列名同名变量顺序要和sql结果列的顺序一致
fetch report into contentId,chapterId,bookContent,createTime,updateTime;
-- 循环遍历
while s<>1 do
-- 执行业务逻辑
set tableNumber = chapterId % 10;
set tableName = concat('book_content',tableNumber);
set bookContent = REPLACE(bookContent,'\'',"\\'");
set @stmt = concat('insert into ',tableName,'(`id`, `chapter_id`, `content`) VALUES (',contentId,', ',chapterId,',\'',bookContent,'\')');
prepare stmt from @stmt;
execute stmt;
deallocate prepare stmt;
fetch report into contentId,chapterId,bookContent,createTime,updateTime;
end while;
-- 关闭游标
close report;
END;
call copyBookContentData();

View File

@ -1,190 +0,0 @@
#
# XXL-JOB v2.4.0-SNAPSHOT
# Copyright (c) 2015-present, xuxueli.
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;
SET NAMES utf8mb4;
CREATE TABLE `xxl_job_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
`schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
`schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
`misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态0-停止1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_id` int(11) NOT NULL COMMENT '任务主键ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
`trigger_msg` text COMMENT '调度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
`handle_msg` text COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态0-默认、1-无需告警、2-告警成功、3-告警失败',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`),
KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_logglue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任务主键ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
`title` varchar(12) NOT NULL COMMENT '执行器名称',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型0=自动注册、1=手动录入',
`address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`role` tinyint(4) NOT NULL COMMENT '角色0-普通用户、1-管理员',
`permission` varchar(255) DEFAULT NULL COMMENT '权限执行器ID列表多个逗号分割',
PRIMARY KEY (`id`),
UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' );
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');
commit;
-- 增加 novel 任务执行器和同步小说数据到 Elasticsearch 的任务
-- 增加 novel 任务执行器和同步小说数据到 Elasticsearch 的任务
INSERT INTO `xxl_job`.`xxl_job_group` ( `app_name`, `title`, `address_type`, `address_list`, `update_time` )
VALUES
(
'xxl-job-executor-novel',
'novel 任务执行器',
0,
NULL,
now()
);
INSERT INTO `xxl_job`.`xxl_job_info` (
`job_group`,
`job_desc`,
`add_time`,
`update_time`,
`author`,
`alarm_email`,
`schedule_type`,
`schedule_conf`,
`misfire_strategy`,
`executor_route_strategy`,
`executor_handler`,
`executor_param`,
`executor_block_strategy`,
`executor_timeout`,
`executor_fail_retry_count`,
`glue_type`,
`glue_source`,
`glue_remark`,
`glue_updatetime`,
`child_jobid`,
`trigger_status`,
`trigger_last_time`,
`trigger_next_time`
)
VALUES
(
(SELECT
id
FROM
xxl_job_group
WHERE
app_name = 'xxl-job-executor-novel'),
'同步小说数据到 Elasticsearch',
now(),
now(),
'xxyopen',
'',
'CRON',
'0 0 0 1 * ?',
'DO_NOTHING',
'FIRST',
'saveToEsJobHandler',
'',
'SERIAL_EXECUTION',
0,
0,
'BEAN',
'',
'GLUE代码初始化',
now(),
'',
0,
0,
0
);

View File

@ -1,3 +0,0 @@
IntelliJ IDEA 中导入 `intellij-java-google-style.xml` 文件:
`Preferences` => `Editor` => `Code Style` => `Java` => `Schema` => `Import Schema`

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<code_scheme name="GoogleStyle">
<codeStyleSettings language="JAVA">
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false"/>
<option name="KEEP_BLANK_LINES_IN_CODE" value="1"/>
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1"/>
<option name="ALIGN_MULTILINE_PARAMETERS" value="false"/>
<option name="ALIGN_MULTILINE_RESOURCES" value="false"/>
<option name="ALIGN_MULTILINE_FOR" value="false"/>
<option name="CALL_PARAMETERS_WRAP" value="1"/>
<option name="METHOD_PARAMETERS_WRAP" value="1"/>
<option name="EXTENDS_LIST_WRAP" value="1"/>
<option name="THROWS_KEYWORD_WRAP" value="1"/>
<option name="METHOD_CALL_CHAIN_WRAP" value="1"/>
<option name="BINARY_OPERATION_WRAP" value="1"/>
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true"/>
<option name="TERNARY_OPERATION_WRAP" value="1"/>
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true"/>
<option name="FOR_STATEMENT_WRAP" value="1"/>
<option name="ARRAY_INITIALIZER_WRAP" value="1"/>
<option name="WRAP_COMMENTS" value="true"/>
<option name="IF_BRACE_FORCE" value="3"/>
<option name="DOWHILE_BRACE_FORCE" value="3"/>
<option name="WHILE_BRACE_FORCE" value="3"/>
<option name="FOR_BRACE_FORCE" value="3"/>
<option name="PARENT_SETTINGS_INSTALLED" value="true"/>
<indentOptions>
<option name="INDENT_SIZE" value="4"/>
<option name="CONTINUATION_INDENT_SIZE" value="4"/>
<option name="TAB_SIZE" value="4"/>
</indentOptions>
</codeStyleSettings>
</code_scheme>

199
pom.xml
View File

@ -1,43 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version> <version>3.0.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>io.github.xxyopen</groupId> <groupId>io.github.xxyopen</groupId>
<artifactId>novel</artifactId> <artifactId>novel</artifactId>
<version>3.5.1-SNAPSHOT</version> <version>3.0.0</version>
<name>novel</name> <name>novel</name>
<description>Spring Boot 3 + Vue 3 构建的前后端分离小说系统</description> <description>Spring Boot 3 + Vue 3 构建的前后端分离小说系统</description>
<properties> <properties>
<java.version>21</java.version> <java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version> <mybatis-plus.version>3.5.1</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.1</mybatis-plus-generator.version> <spring.version>6.0.0-SNAPSHOT</spring.version>
<jjwt.version>0.11.5</jjwt.version> <jjwt.version>0.11.5</jjwt.version>
<xxl-job.version>2.3.1</xxl-job.version>
<sentinel.version>1.8.4</sentinel.version>
<shardingsphere-jdbc.version>5.5.1</shardingsphere-jdbc.version>
<redisson.version>3.19.1</redisson.version>
<spring-boot-admin.version>3.0.0-M1</spring-boot-admin.version>
<springdoc-openapi.version>2.5.0</springdoc-openapi.version>
<logbook.version>3.9.0</logbook.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <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> </dependency>
<!-- mybatis-plus --> <!-- mybatis-plus -->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> <artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version> <version>${mybatis-plus.version}</version>
</dependency> </dependency>
@ -45,7 +49,7 @@
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId> <artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version> <version>${mybatis-plus.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
@ -83,7 +87,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>${jjwt.version}</version> <version>${jjwt.version}</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
@ -95,97 +99,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>mysql</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId> <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- MQ 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- XXL-JOB 相关 -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${xxl-job.version}</version>
</dependency>
<!-- sentinel 相关 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>${sentinel.version}</version>
</dependency>
<!-- ShardingSphere-JDBC -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>${shardingsphere-jdbc.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot 管理和监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
@ -203,33 +118,8 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>${logbook.version}</version>
</dependency>
</dependencies> </dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
@ -244,40 +134,43 @@
</excludes> </excludes>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!--指定 Java 编译器的 -source 参数 -->
<source>${java.version}</source>
<!--指定 Java 编译器的 -target 参数 -->
<target>${java.version}</target>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
<repositories> <repositories>
<repository> <repository>
<id>aliyun</id> <id>spring-milestones</id>
<url>https://maven.aliyun.com/repository/public</url> <name>Spring Milestones</name>
<releases> <url>https://repo.spring.io/milestone</url>
<enabled>true</enabled>
</releases>
<snapshots> <snapshots>
<enabled>false</enabled> <enabled>false</enabled>
</snapshots> </snapshots>
</repository> </repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories> </repositories>
<pluginRepositories> <pluginRepositories>
<pluginRepository> <pluginRepository>
<id>aliyun</id> <id>spring-milestones</id>
<url>https://maven.aliyun.com/repository/public</url> <name>Spring Milestones</name>
<releases> <url>https://repo.spring.io/milestone</url>
<enabled>true</enabled>
</releases>
<snapshots> <snapshots>
<enabled>false</enabled> <enabled>false</enabled>
</snapshots> </snapshots>
</pluginRepository> </pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories> </pluginRepositories>
</project> </project>

View File

@ -1,28 +1,20 @@
package io.github.xxyopen.novel; package io.github.xxyopen.novel;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Map; import java.util.Map;
@SpringBootApplication @SpringBootApplication
@MapperScan("io.github.xxyopen.novel.dao.mapper") @MapperScan("io.github.xxyopen.novel.dao.mapper")
@EnableCaching @EnableCaching
@EnableScheduling
@Slf4j @Slf4j
public class NovelApplication { public class NovelApplication {
@ -31,35 +23,16 @@ public class NovelApplication {
} }
@Bean @Bean
public CommandLineRunner commandLineRunner(ApplicationContext context, DataSource dataSource) { public CommandLineRunner commandLineRunner(ApplicationContext context){
return args -> { return args -> {
Map<String, CacheManager> beans = context.getBeansOfType(CacheManager.class); Map<String, CacheManager> beans = context.getBeansOfType(CacheManager.class);
log.info("加载了如下缓存管理器:"); log.info("加载了如下缓存管理器:");
beans.forEach((k, v) -> { beans.forEach((k,v)->{
log.info("{}:{}", k, v.getClass().getName()); log.info("{}:{}",k,v.getClass().getName());
log.info("缓存:{}", v.getCacheNames()); log.info("缓存:{}",v.getCacheNames());
}); });
if(dataSource instanceof HikariDataSource hikariDataSource) {
// 如果使用的是HikariDataSource需要提前创建连接池而不是在第一次访问数据库时才创建提高第一次访问接口的速度
log.info("创建连接池...");
try (Connection connection = dataSource.getConnection()) {
log.info("最小空闲连接数:{}", hikariDataSource.getMinimumIdle());
log.info("最大连接数:{}", hikariDataSource.getMaximumPoolSize());
log.info("创建连接池完成.");
log.info("数据库:{}", connection.getMetaData().getDatabaseProductName());
log.info("数据库版本:{}", connection.getMetaData().getDatabaseProductVersion());
}
}
}; };
} }
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(requests -> requests.anyRequest().hasRole("ENDPOINT_ADMIN"));
http.httpBasic();
return http.build();
}
} }

View File

@ -1,83 +0,0 @@
package io.github.xxyopen.novel.controller.author;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 作家后台-AI模块API控制器
*
* @author xiongxiaoyang
* @date 2025/2/19
*/
@Tag(name = "AiController", description = "作家后台-AI模块")
@SecurityRequirement(name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME)
@RestController
@RequestMapping(ApiRouterConsts.API_AUTHOR_AI_URL_PREFIX)
@RequiredArgsConstructor
public class AuthorAiController {
private final ChatClient chatClient;
/**
* AI扩写
*/
@Operation(summary = "AI扩写接口")
@PostMapping("/expand")
public RestResp<String> expandText(@RequestParam("text") String text, @RequestParam("ratio") Double ratio) {
String prompt = "请将以下文本扩写为原长度的" + ratio/100 + "倍:" + text;
return RestResp.ok(chatClient.prompt()
.user(prompt)
.call()
.content());
}
/**
* AI缩写
*/
@Operation(summary = "AI缩写接口")
@PostMapping("/condense")
public RestResp<String> condenseText(@RequestParam("text") String text, @RequestParam("ratio") Integer ratio) {
String prompt = "请将以下文本缩写为原长度的" + 100/ratio + "分之一:" + text;
return RestResp.ok(chatClient.prompt()
.user(prompt)
.call()
.content());
}
/**
* AI续写
*/
@Operation(summary = "AI续写接口")
@PostMapping("/continue")
public RestResp<String> continueText(@RequestParam("text") String text, @RequestParam("length") Integer length) {
String prompt = "请续写以下文本,续写长度约为" + length + "字:" + text;
return RestResp.ok(chatClient.prompt()
.user(prompt)
.call()
.content());
}
/**
* AI润色
*/
@Operation(summary = "AI润色接口")
@PostMapping("/polish")
public RestResp<String> polishText(@RequestParam("text") String text) {
String prompt = "请润色优化以下文本,保持原意:" + text;
return RestResp.ok(chatClient.prompt()
.user(prompt)
.call()
.content());
}
}

View File

@ -1,135 +0,0 @@
package io.github.xxyopen.novel.controller.author;
import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.common.req.PageReqDto;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.dto.req.AuthorRegisterReqDto;
import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterUpdateReqDto;
import io.github.xxyopen.novel.dto.resp.BookChapterRespDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.dto.resp.ChapterContentRespDto;
import io.github.xxyopen.novel.service.AuthorService;
import io.github.xxyopen.novel.service.BookService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.*;
/**
* 作家后台-作家模块 API 控制器
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Tag(name = "AuthorController", description = "作家后台-作者模块")
@SecurityRequirement(name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME)
@RestController
@RequestMapping(ApiRouterConsts.API_AUTHOR_URL_PREFIX)
@RequiredArgsConstructor
public class AuthorController {
private final AuthorService authorService;
private final BookService bookService;
/**
* 作家注册接口
*/
@Operation(summary = "作家注册接口")
@PostMapping("register")
public RestResp<Void> register(@Valid @RequestBody AuthorRegisterReqDto dto) {
dto.setUserId(UserHolder.getUserId());
return authorService.register(dto);
}
/**
* 查询作家状态接口
*/
@Operation(summary = "作家状态查询接口")
@GetMapping("status")
public RestResp<Integer> getStatus() {
return authorService.getStatus(UserHolder.getUserId());
}
/**
* 小说发布接口
*/
@Operation(summary = "小说发布接口")
@PostMapping("book")
public RestResp<Void> publishBook(@Valid @RequestBody BookAddReqDto dto) {
return bookService.saveBook(dto);
}
/**
* 小说发布列表查询接口
*/
@Operation(summary = "小说发布列表查询接口")
@GetMapping("books")
public RestResp<PageRespDto<BookInfoRespDto>> listBooks(@ParameterObject PageReqDto dto) {
return bookService.listAuthorBooks(dto);
}
/**
* 小说章节发布接口
*/
@Operation(summary = "小说章节发布接口")
@PostMapping("book/chapter/{bookId}")
public RestResp<Void> publishBookChapter(
@Parameter(description = "小说ID") @PathVariable("bookId") Long bookId,
@Valid @RequestBody ChapterAddReqDto dto) {
dto.setBookId(bookId);
return bookService.saveBookChapter(dto);
}
/**
* 小说章节删除接口
*/
@Operation(summary = "小说章节删除接口")
@DeleteMapping("book/chapter/{chapterId}")
public RestResp<Void> deleteBookChapter(
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) {
return bookService.deleteBookChapter(chapterId);
}
/**
* 小说章节查询接口
*/
@Operation(summary = "小说章节查询接口")
@GetMapping("book/chapter/{chapterId}")
public RestResp<ChapterContentRespDto> getBookChapter(
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) {
return bookService.getBookChapter(chapterId);
}
/**
* 小说章节更新接口
*/
@Operation(summary = "小说章节更新接口")
@PutMapping("book/chapter/{chapterId}")
public RestResp<Void> updateBookChapter(
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId,
@Valid @RequestBody ChapterUpdateReqDto dto) {
return bookService.updateBookChapter(chapterId, dto);
}
/**
* 小说章节发布列表查询接口
*/
@Operation(summary = "小说章节发布列表查询接口")
@GetMapping("book/chapters/{bookId}")
public RestResp<PageRespDto<BookChapterRespDto>> listBookChapters(
@Parameter(description = "小说ID") @PathVariable("bookId") Long bookId,
@ParameterObject PageReqDto dto) {
return bookService.listBookChapters(bookId, dto);
}
}

View File

@ -1,34 +1,23 @@
package io.github.xxyopen.novel.controller.front; package io.github.xxyopen.novel.controller.front;
import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.dto.resp.BookCategoryRespDto; import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.resp.BookChapterAboutRespDto; import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookChapterRespDto; import io.github.xxyopen.novel.dto.resp.*;
import io.github.xxyopen.novel.dto.resp.BookCommentRespDto;
import io.github.xxyopen.novel.dto.resp.BookContentAboutRespDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.dto.resp.BookRankRespDto;
import io.github.xxyopen.novel.service.BookService; import io.github.xxyopen.novel.service.BookService;
import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Parameter; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 前台门户-小说模块 API 控制器 * 小说模块 API 接口
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/14 * @date 2022/5/14
*/ */
@Tag(name = "BookController", description = "前台门户-小说模块")
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_FRONT_BOOK_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_FRONT_BOOK_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@ -39,96 +28,86 @@ public class BookController {
/** /**
* 小说分类列表查询接口 * 小说分类列表查询接口
*/ */
@Operation(summary = "小说分类列表查询接口")
@GetMapping("category/list") @GetMapping("category/list")
public RestResp<List<BookCategoryRespDto>> listCategory( public RestResp<List<BookCategoryRespDto>> listCategory(Integer workDirection) {
@Parameter(description = "作品方向", required = true) Integer workDirection) {
return bookService.listCategory(workDirection); return bookService.listCategory(workDirection);
} }
/**
* 小说搜索接口
*/
@GetMapping("search_list")
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
return bookService.searchBooks(condition);
}
/** /**
* 小说信息查询接口 * 小说信息查询接口
*/ */
@Operation(summary = "小说信息查询接口")
@GetMapping("{id}") @GetMapping("{id}")
public RestResp<BookInfoRespDto> getBookById( public RestResp<BookInfoRespDto> getBookById(@PathVariable("id") Long bookId) {
@Parameter(description = "小说 ID") @PathVariable("id") Long bookId) {
return bookService.getBookById(bookId); return bookService.getBookById(bookId);
} }
/** /**
* 增加小说点击量接口 * 增加小说点击量接口
*/ */
@Operation(summary = "增加小说点击量接口")
@PostMapping("visit") @PostMapping("visit")
public RestResp<Void> addVisitCount(@Parameter(description = "小说ID") Long bookId) { public RestResp<Void> addVisitCount(Long bookId) {
return bookService.addVisitCount(bookId); return bookService.addVisitCount(bookId);
} }
/** /**
* 小说最新章节相关信息查询接口 * 小说最新章节相关信息查询接口
*/ */
@Operation(summary = "小说最新章节相关信息查询接口")
@GetMapping("last_chapter/about") @GetMapping("last_chapter/about")
public RestResp<BookChapterAboutRespDto> getLastChapterAbout( public RestResp<BookChapterAboutRespDto> getLastChapterAbout(Long bookId) {
@Parameter(description = "小说ID") Long bookId) {
return bookService.getLastChapterAbout(bookId); return bookService.getLastChapterAbout(bookId);
} }
/** /**
* 小说推荐列表查询接口 * 小说推荐列表查询接口
*/ */
@Operation(summary = "小说推荐列表查询接口")
@GetMapping("rec_list") @GetMapping("rec_list")
public RestResp<List<BookInfoRespDto>> listRecBooks( public RestResp<List<BookInfoRespDto>> listRecBooks(Long bookId) throws NoSuchAlgorithmException {
@Parameter(description = "小说ID") Long bookId) throws NoSuchAlgorithmException {
return bookService.listRecBooks(bookId); return bookService.listRecBooks(bookId);
} }
/** /**
* 小说章节列表查询接口 * 小说章节列表查询接口
*/ */
@Operation(summary = "小说章节列表查询接口")
@GetMapping("chapter/list") @GetMapping("chapter/list")
public RestResp<List<BookChapterRespDto>> listChapters( public RestResp<List<BookChapterRespDto>> listChapters(Long bookId) {
@Parameter(description = "小说ID") Long bookId) {
return bookService.listChapters(bookId); return bookService.listChapters(bookId);
} }
/** /**
* 小说内容相关信息查询接口 * 小说内容相关信息查询接口
*/ */
@Operation(summary = "小说内容相关信息查询接口")
@GetMapping("content/{chapterId}") @GetMapping("content/{chapterId}")
public RestResp<BookContentAboutRespDto> getBookContentAbout( public RestResp<BookContentAboutRespDto> getBookContentAbout(@PathVariable("chapterId") Long chapterId) {
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) {
return bookService.getBookContentAbout(chapterId); return bookService.getBookContentAbout(chapterId);
} }
/** /**
* 获取上一章节ID接口 * 获取上一章节ID接口
*/ */
@Operation(summary = "获取上一章节ID接口")
@GetMapping("pre_chapter_id/{chapterId}") @GetMapping("pre_chapter_id/{chapterId}")
public RestResp<Long> getPreChapterId( public RestResp<Long> getPreChapterId(@PathVariable("chapterId") Long chapterId) {
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) {
return bookService.getPreChapterId(chapterId); return bookService.getPreChapterId(chapterId);
} }
/** /**
* 获取下一章节ID接口 * 获取下一章节ID接口
*/ */
@Operation(summary = "获取下一章节ID接口")
@GetMapping("next_chapter_id/{chapterId}") @GetMapping("next_chapter_id/{chapterId}")
public RestResp<Long> getNextChapterId( public RestResp<Long> getNextChapterId(@PathVariable("chapterId") Long chapterId) {
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) {
return bookService.getNextChapterId(chapterId); return bookService.getNextChapterId(chapterId);
} }
/** /**
* 小说点击榜查询接口 * 小说点击榜查询接口
*/ */
@Operation(summary = "小说点击榜查询接口")
@GetMapping("visit_rank") @GetMapping("visit_rank")
public RestResp<List<BookRankRespDto>> listVisitRankBooks() { public RestResp<List<BookRankRespDto>> listVisitRankBooks() {
return bookService.listVisitRankBooks(); return bookService.listVisitRankBooks();
@ -137,7 +116,6 @@ public class BookController {
/** /**
* 小说新书榜查询接口 * 小说新书榜查询接口
*/ */
@Operation(summary = "小说新书榜查询接口")
@GetMapping("newest_rank") @GetMapping("newest_rank")
public RestResp<List<BookRankRespDto>> listNewestRankBooks() { public RestResp<List<BookRankRespDto>> listNewestRankBooks() {
return bookService.listNewestRankBooks(); return bookService.listNewestRankBooks();
@ -146,7 +124,6 @@ public class BookController {
/** /**
* 小说更新榜查询接口 * 小说更新榜查询接口
*/ */
@Operation(summary = "小说更新榜查询接口")
@GetMapping("update_rank") @GetMapping("update_rank")
public RestResp<List<BookRankRespDto>> listUpdateRankBooks() { public RestResp<List<BookRankRespDto>> listUpdateRankBooks() {
return bookService.listUpdateRankBooks(); return bookService.listUpdateRankBooks();
@ -155,10 +132,8 @@ public class BookController {
/** /**
* 小说最新评论查询接口 * 小说最新评论查询接口
*/ */
@Operation(summary = "小说最新评论查询接口")
@GetMapping("comment/newest_list") @GetMapping("comment/newest_list")
public RestResp<BookCommentRespDto> listNewestComments( public RestResp<BookCommentRespDto> listNewestComments(Long bookId) {
@Parameter(description = "小说ID") Long bookId) {
return bookService.listNewestComments(bookId); return bookService.listNewestComments(bookId);
} }

View File

@ -5,10 +5,7 @@ import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.resp.HomeBookRespDto; import io.github.xxyopen.novel.dto.resp.HomeBookRespDto;
import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto; import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto;
import io.github.xxyopen.novel.service.HomeService; import io.github.xxyopen.novel.service.HomeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -16,16 +13,14 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
/** /**
* 前台门户-首页模块 API 控制器 * 首页模块 API 接口
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/12 * @date 2022/5/12
*/ */
@Tag(name = "HomeController", description = "前台门户-首页模块")
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_FRONT_HOME_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_FRONT_HOME_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class HomeController { public class HomeController {
private final HomeService homeService; private final HomeService homeService;
@ -33,18 +28,14 @@ public class HomeController {
/** /**
* 首页小说推荐查询接口 * 首页小说推荐查询接口
*/ */
@Operation(summary = "首页小说推荐查询接口")
@GetMapping("books") @GetMapping("books")
public RestResp<List<HomeBookRespDto>> listHomeBooks() { public RestResp<List<HomeBookRespDto>> listHomeBooks() {
// 测试虚拟线程处理请求
log.debug("处理请求的线程:{}", Thread.currentThread());
return homeService.listHomeBooks(); return homeService.listHomeBooks();
} }
/** /**
* 首页友情链接列表查询接口 * 首页友情链接列表查询接口
*/ */
@Operation(summary = "首页友情链接列表查询接口")
@GetMapping("friend_Link/list") @GetMapping("friend_Link/list")
public RestResp<List<HomeFriendLinkRespDto>> listHomeFriendLinks() { public RestResp<List<HomeFriendLinkRespDto>> listHomeFriendLinks() {
return homeService.listHomeFriendLinks(); return homeService.listHomeFriendLinks();

View File

@ -1,26 +1,23 @@
package io.github.xxyopen.novel.controller.front; package io.github.xxyopen.novel.controller.front;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto; import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto;
import io.github.xxyopen.novel.service.NewsService; import io.github.xxyopen.novel.service.NewsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* 前台门户-新闻模块 API 控制器 * 新闻模块 API 接口
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/12 * @date 2022/5/12
*/ */
@Tag(name = "NewsController", description = "前台门户-新闻模块")
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_FRONT_NEWS_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_FRONT_NEWS_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@ -31,7 +28,6 @@ public class NewsController {
/** /**
* 最新新闻列表查询接口 * 最新新闻列表查询接口
*/ */
@Operation(summary = "最新新闻列表查询接口")
@GetMapping("latest_list") @GetMapping("latest_list")
public RestResp<List<NewsInfoRespDto>> listLatestNews() { public RestResp<List<NewsInfoRespDto>> listLatestNews() {
return newsService.listLatestNews(); return newsService.listLatestNews();
@ -40,10 +36,8 @@ public class NewsController {
/** /**
* 新闻信息查询接口 * 新闻信息查询接口
*/ */
@Operation(summary = "新闻信息查询接口")
@GetMapping("{id}") @GetMapping("{id}")
public RestResp<NewsInfoRespDto> getNews( public RestResp<NewsInfoRespDto> getNews(@PathVariable Long id) {
@Parameter(description = "新闻ID") @PathVariable Long id) {
return newsService.getNews(id); return newsService.getNews(id);
} }
} }

View File

@ -4,25 +4,19 @@ import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.dto.resp.ImgVerifyCodeRespDto; import io.github.xxyopen.novel.dto.resp.ImgVerifyCodeRespDto;
import io.github.xxyopen.novel.service.ResourceService; import io.github.xxyopen.novel.service.ResourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/** /**
* 前台门户-资源(图片/视频/文档)模块 API 控制器 * 资源图片/视频/文档相关 控制器
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/17 * @date 2022/5/17
*/ */
@Tag(name = "ResourceController", description = "前台门户-资源模块")
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_FRONT_RESOURCE_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_FRONT_RESOURCE_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@ -33,20 +27,9 @@ public class ResourceController {
/** /**
* 获取图片验证码接口 * 获取图片验证码接口
*/ */
@Operation(summary = "获取图片验证码接口")
@GetMapping("img_verify_code") @GetMapping("img_verify_code")
public RestResp<ImgVerifyCodeRespDto> getImgVerifyCode() throws IOException { public RestResp<ImgVerifyCodeRespDto> getImgVerifyCode() throws IOException {
return resourceService.getImgVerifyCode(); return resourceService.getImgVerifyCode();
} }
/**
* 图片上传接口
*/
@Operation(summary = "图片上传接口")
@PostMapping("/image")
RestResp<String> uploadImage(
@Parameter(description = "上传文件") @RequestParam("file") MultipartFile file) {
return resourceService.uploadImage(file);
}
} }

View File

@ -1,41 +0,0 @@
package io.github.xxyopen.novel.controller.front;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.SearchService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 前台门户-搜索模块 API 控制器
*
* @author xiongxiaoyang
* @date 2022/5/27
*/
@Tag(name = "SearchController", description = "前台门户-搜索模块")
@RestController
@RequestMapping(ApiRouterConsts.API_FRONT_SEARCH_URL_PREFIX)
@RequiredArgsConstructor
public class SearchController {
private final SearchService searchService;
/**
* 小说搜索接口
*/
@Operation(summary = "小说搜索接口")
@GetMapping("books")
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(
@ParameterObject BookSearchReqDto condition) {
return searchService.searchBooks(condition);
}
}

View File

@ -1,37 +1,26 @@
package io.github.xxyopen.novel.controller.front; package io.github.xxyopen.novel.controller.front;
import io.github.xxyopen.novel.core.auth.UserHolder; import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.common.req.PageReqDto;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto; import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.req.UserInfoUptReqDto; import io.github.xxyopen.novel.dto.req.UserInfoUptReqDto;
import io.github.xxyopen.novel.dto.req.UserLoginReqDto; import io.github.xxyopen.novel.dto.req.UserLoginReqDto;
import io.github.xxyopen.novel.dto.req.UserRegisterReqDto; import io.github.xxyopen.novel.dto.req.UserRegisterReqDto;
import io.github.xxyopen.novel.dto.resp.UserCommentRespDto;
import io.github.xxyopen.novel.dto.resp.UserInfoRespDto;
import io.github.xxyopen.novel.dto.resp.UserLoginRespDto; import io.github.xxyopen.novel.dto.resp.UserLoginRespDto;
import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto; import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto;
import io.github.xxyopen.novel.service.BookService; import io.github.xxyopen.novel.service.BookService;
import io.github.xxyopen.novel.service.UserService; import io.github.xxyopen.novel.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
/** /**
* 前台门户-会员模块 API 控制器 * 会员模块相关 控制器
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/17 * @date 2022/5/17
*/ */
@Tag(name = "UserController", description = "前台门户-会员模块")
@SecurityRequirement(name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME)
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_FRONT_USER_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_FRONT_USER_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@ -44,7 +33,6 @@ public class UserController {
/** /**
* 用户注册接口 * 用户注册接口
*/ */
@Operation(summary = "用户注册接口")
@PostMapping("register") @PostMapping("register")
public RestResp<UserRegisterRespDto> register(@Valid @RequestBody UserRegisterReqDto dto) { public RestResp<UserRegisterRespDto> register(@Valid @RequestBody UserRegisterReqDto dto) {
return userService.register(dto); return userService.register(dto);
@ -53,25 +41,15 @@ public class UserController {
/** /**
* 用户登录接口 * 用户登录接口
*/ */
@Operation(summary = "用户登录接口")
@PostMapping("login") @PostMapping("login")
public RestResp<UserLoginRespDto> login(@Valid @RequestBody UserLoginReqDto dto) { public RestResp<UserLoginRespDto> login(@Valid @RequestBody UserLoginReqDto dto) {
return userService.login(dto); return userService.login(dto);
} }
/**
* 用户信息查询接口
*/
@Operation(summary = "用户信息查询接口")
@GetMapping
public RestResp<UserInfoRespDto> getUserInfo() {
return userService.getUserInfo(UserHolder.getUserId());
}
/** /**
* 用户信息修改接口 * 用户信息修改接口
*/ */
@Operation(summary = "用户信息修改接口")
@PutMapping @PutMapping
public RestResp<Void> updateUserInfo(@Valid @RequestBody UserInfoUptReqDto dto) { public RestResp<Void> updateUserInfo(@Valid @RequestBody UserInfoUptReqDto dto) {
dto.setUserId(UserHolder.getUserId()); dto.setUserId(UserHolder.getUserId());
@ -81,7 +59,6 @@ public class UserController {
/** /**
* 用户反馈提交接口 * 用户反馈提交接口
*/ */
@Operation(summary = "用户反馈提交接口")
@PostMapping("feedback") @PostMapping("feedback")
public RestResp<Void> submitFeedback(@RequestBody String content) { public RestResp<Void> submitFeedback(@RequestBody String content) {
return userService.saveFeedback(UserHolder.getUserId(), content); return userService.saveFeedback(UserHolder.getUserId(), content);
@ -90,16 +67,14 @@ public class UserController {
/** /**
* 用户反馈删除接口 * 用户反馈删除接口
*/ */
@Operation(summary = "用户反馈删除接口")
@DeleteMapping("feedback/{id}") @DeleteMapping("feedback/{id}")
public RestResp<Void> deleteFeedback(@Parameter(description = "反馈ID") @PathVariable Long id) { public RestResp<Void> deleteFeedback(@PathVariable Long id) {
return userService.deleteFeedback(UserHolder.getUserId(), id); return userService.deleteFeedback(UserHolder.getUserId(), id);
} }
/** /**
* 发表评论接口 * 发表评论接口
*/ */
@Operation(summary = "发表评论接口")
@PostMapping("comment") @PostMapping("comment")
public RestResp<Void> comment(@Valid @RequestBody UserCommentReqDto dto) { public RestResp<Void> comment(@Valid @RequestBody UserCommentReqDto dto) {
dto.setUserId(UserHolder.getUserId()); dto.setUserId(UserHolder.getUserId());
@ -109,38 +84,27 @@ public class UserController {
/** /**
* 修改评论接口 * 修改评论接口
*/ */
@Operation(summary = "修改评论接口")
@PutMapping("comment/{id}") @PutMapping("comment/{id}")
public RestResp<Void> updateComment(@Parameter(description = "评论ID") @PathVariable Long id, public RestResp<Void> updateComment(@PathVariable Long id, String content) {
String content) {
return bookService.updateComment(UserHolder.getUserId(), id, content); return bookService.updateComment(UserHolder.getUserId(), id, content);
} }
/** /**
* 删除评论接口 * 删除评论接口
*/ */
@Operation(summary = "删除评论接口")
@DeleteMapping("comment/{id}") @DeleteMapping("comment/{id}")
public RestResp<Void> deleteComment(@Parameter(description = "评论ID") @PathVariable Long id) { public RestResp<Void> deleteComment(@PathVariable Long id) {
return bookService.deleteComment(UserHolder.getUserId(), id); return bookService.deleteComment(UserHolder.getUserId(), id);
} }
/** /**
* 查询书架状态接口 0-不在书架 1-已在书架 * 查询书架状态接口
* 0-不在书架
* 1-已在书架
*/ */
@Operation(summary = "查询书架状态接口")
@GetMapping("bookshelf_status") @GetMapping("bookshelf_status")
public RestResp<Integer> getBookshelfStatus(@Parameter(description = "小说ID") String bookId) { public RestResp<Integer> getBookshelfStatus(@RequestBody String bookId) {
return userService.getBookshelfStatus(UserHolder.getUserId(), bookId); return userService.getBookshelfStatus(UserHolder.getUserId(), bookId);
} }
/**
* 分页查询评论
*/
@Operation(summary = "查询会员评论列表接口")
@GetMapping("comments")
public RestResp<PageRespDto<UserCommentRespDto>> listComments(PageReqDto pageReqDto) {
return bookService.listComments(UserHolder.getUserId(), pageReqDto);
}
} }

View File

@ -1,23 +0,0 @@
package io.github.xxyopen.novel.core.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 "";
}

View File

@ -1,31 +0,0 @@
package io.github.xxyopen.novel.core.annotation;
import io.github.xxyopen.novel.core.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;
}

View File

@ -1,86 +0,0 @@
package io.github.xxyopen.novel.core.aspect;
import io.github.xxyopen.novel.core.annotation.Key;
import io.github.xxyopen.novel.core.annotation.Lock;
import io.github.xxyopen.novel.core.common.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.core.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);
}
}

View File

@ -5,7 +5,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* 平台后台管理系统 认证授权策略 * 平台后台管理系统 认证策略
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/18 * @date 2022/5/18
@ -15,8 +15,7 @@ import org.springframework.stereotype.Component;
public class AdminAuthStrategy implements AuthStrategy { public class AdminAuthStrategy implements AuthStrategy {
@Override @Override
public void auth(String token, String requestUri) throws BusinessException { public void auth(String token) throws BusinessException {
// TODO 平台后台 token 校验 // TODO 平台后台 token 校验
} }
} }

View File

@ -5,10 +5,11 @@ import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts; import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.core.util.JwtUtils; import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.dto.UserInfoDto; import io.github.xxyopen.novel.dto.UserInfoDto;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager; import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import java.util.Objects;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.Objects;
/** /**
* 策略模式实现用户认证授权功能 * 策略模式实现用户认证授权功能
* *
@ -18,24 +19,23 @@ import org.springframework.util.StringUtils;
public interface AuthStrategy { public interface AuthStrategy {
/** /**
* 用户认证授权 * 请求用户认证
* 如果后面需要扩展到对每一个URI都进行权限控制那么此方法可以加一个参数来接收用户请求的URI
* *
* @param token 登录 token * @param token 登录 token
* @param requestUri 请求的 URI
* @throws BusinessException 认证失败则抛出业务异常 * @throws BusinessException 认证失败则抛出业务异常
*/ */
void auth(String token, String requestUri) throws BusinessException; void auth(String token) throws BusinessException;
/** /**
* 前台多系统单点登录统一账号认证授权门户系统作家系统以及后面会扩展的漫画系统和视频系统等 * 前台多系统单点登录统一账号认证门户系统作家系统以及后面会扩展的漫画系统和视频系统等
* *
* @param jwtUtils jwt 工具 * @param jwtUtils jwt 工具
* @param userInfoCacheManager 用户缓存管理对象 * @param userInfoCacheManager 用户缓存管理对象
* @param token token 登录 token * @param token token 登录 token
* @return 用户ID * @return 用户ID
*/ */
default Long authSSO(JwtUtils jwtUtils, UserInfoCacheManager userInfoCacheManager, default Long authSSO(JwtUtils jwtUtils, UserInfoCacheManager userInfoCacheManager, String token) {
String token) {
if (!StringUtils.hasText(token)) { if (!StringUtils.hasText(token)) {
// token 为空 // token 为空
throw new BusinessException(ErrorCodeEnum.USER_LOGIN_EXPIRED); throw new BusinessException(ErrorCodeEnum.USER_LOGIN_EXPIRED);
@ -55,5 +55,4 @@ public interface AuthStrategy {
// 返回 userId // 返回 userId
return userId; return userId;
} }
} }

View File

@ -2,18 +2,17 @@ package io.github.xxyopen.novel.core.auth;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum; import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.exception.BusinessException; import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.util.JwtUtils; import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.dto.AuthorInfoDto; import io.github.xxyopen.novel.dto.AuthorInfoDto;
import io.github.xxyopen.novel.manager.cache.AuthorInfoCacheManager; import io.github.xxyopen.novel.manager.AuthorInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager; import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Objects;
/** /**
* 作家后台管理系统 认证授权策略 * 作家后台管理系统 认证策略
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/18 * @date 2022/5/18
@ -28,25 +27,14 @@ public class AuthorAuthStrategy implements AuthStrategy {
private final AuthorInfoCacheManager authorInfoCacheManager; private final AuthorInfoCacheManager authorInfoCacheManager;
/**
* 不需要进行作家权限认证的 URI
*/
private static final List<String> EXCLUDE_URI = List.of(
ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/register",
ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/status"
);
@Override @Override
public void auth(String token, String requestUri) throws BusinessException { public void auth(String token) throws BusinessException {
// 统一账号认证 // 统一账号认证
Long userId = authSSO(jwtUtils, userInfoCacheManager, token); Long userId = authSSO(jwtUtils, userInfoCacheManager, token);
if (EXCLUDE_URI.contains(requestUri)) {
// 该请求不需要进行作家权限认证
return;
}
// 作家权限认证 // 作家权限认证
AuthorInfoDto authorInfo = authorInfoCacheManager.getAuthor(userId); AuthorInfoDto authorInfo = authorInfoCacheManager.getAuthor(userId);
if (Objects.isNull(authorInfo)) { if(Objects.isNull(authorInfo)){
// 作家账号不存在无权访问作家专区 // 作家账号不存在无权访问作家专区
throw new BusinessException(ErrorCodeEnum.USER_UN_AUTH); throw new BusinessException(ErrorCodeEnum.USER_UN_AUTH);
} }
@ -54,5 +42,4 @@ public class AuthorAuthStrategy implements AuthStrategy {
// 设置作家ID到当前线程 // 设置作家ID到当前线程
UserHolder.setAuthorId(authorInfo.getId()); UserHolder.setAuthorId(authorInfo.getId());
} }
} }

View File

@ -2,12 +2,12 @@ package io.github.xxyopen.novel.core.auth;
import io.github.xxyopen.novel.core.common.exception.BusinessException; import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.util.JwtUtils; import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager; import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* 前台门户系统 认证授权策略 * 前台门户系统 认证策略
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/18 * @date 2022/5/18
@ -21,9 +21,8 @@ public class FrontAuthStrategy implements AuthStrategy {
private final UserInfoCacheManager userInfoCacheManager; private final UserInfoCacheManager userInfoCacheManager;
@Override @Override
public void auth(String token, String requestUri) throws BusinessException { public void auth(String token) throws BusinessException {
// 统一账号认证 // 统一账号认证
authSSO(jwtUtils, userInfoCacheManager, token); authSSO(jwtUtils,userInfoCacheManager,token);
} }
} }

View File

@ -13,12 +13,12 @@ public class UserHolder {
/** /**
* 当前线程用户ID * 当前线程用户ID
*/ * */
private static final ThreadLocal<Long> userIdTL = new ThreadLocal<>(); private static final ThreadLocal<Long> userIdTL = new ThreadLocal<>();
/** /**
* 当前线程作家ID * 当前线程作家ID
*/ * */
private static final ThreadLocal<Long> authorIdTL = new ThreadLocal<>(); private static final ThreadLocal<Long> authorIdTL = new ThreadLocal<>();
public void setUserId(Long userId) { public void setUserId(Long userId) {
@ -37,7 +37,7 @@ public class UserHolder {
return authorIdTL.get(); return authorIdTL.get();
} }
public void clear() { public void clear(){
userIdTL.remove(); userIdTL.remove();
authorIdTL.remove(); authorIdTL.remove();
} }

View File

@ -11,33 +11,30 @@ public class CommonConsts {
/** /**
* *
*/ * */
public static final Integer YES = 1; public static final Integer YES = 1;
public static final String TRUE = "true";
/** /**
* *
*/ * */
public static final Integer NO = 0; public static final Integer NO = 0;
public static final String FALSE = "false";
/** /**
* 性别常量 * 性别常量
*/ * */
public enum SexEnum { public enum SexEnum{
/** /**
* *
*/ * */
MALE(0, ""), MALE(0,""),
/** /**
* *
*/ * */
FEMALE(1, ""); FEMALE(1,"");
SexEnum(int code, String desc) { SexEnum(int code,String desc){
this.code = code; this.code = code;
this.desc = desc; this.desc = desc;
} }

View File

@ -5,12 +5,15 @@ import lombok.Getter;
/** /**
* 错误码枚举类 * 错误码枚举类
* <p> *
* 错误码为字符串类型 5 分成两个部分错误产生来源+四位数字编号 错误产生来源分为 A/B/C A 表示错误来源于用户比如参数错误用户安装版本过低用户支付 超时等问题 B * 错误码为字符串类型 5 分成两个部分错误产生来源+四位数字编号
* 表示错误来源于当前系统往往是业务逻辑出错或程序健壮性差等问题 C 表示错误来源 于第三方服务比如 CDN 服务出错消息投递超时等问题四位数字编号从 0001 9999大类之间的 * 错误产生来源分为 A/B/C A 表示错误来源于用户比如参数错误用户安装版本过低用户支付
* 超时等问题 B 表示错误来源于当前系统往往是业务逻辑出错或程序健壮性差等问题 C 表示错误来源
* 于第三方服务比如 CDN 服务出错消息投递超时等问题四位数字编号从 0001 9999大类之间的
* 步长间距预留 100 * 步长间距预留 100
* <p> *
* 错误码分为一级宏观错误码二级宏观错误码三级宏观错误码 在无法更加具体确定的错误场景中可以直接使用一级宏观错误码 * 错误码分为一级宏观错误码二级宏观错误码三级宏观错误码
* 在无法更加具体确定的错误场景中可以直接使用一级宏观错误码
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/11 * @date 2022/5/11
@ -21,132 +24,103 @@ public enum ErrorCodeEnum {
/** /**
* 正确执行后的返回 * 正确执行后的返回
*/ * */
OK("00000", "一切 ok"), OK("00000","一切 ok"),
/** /**
* 一级宏观错误码用户端错误 * 一级宏观错误码用户端错误
*/ * */
USER_ERROR("A0001", "用户端错误"), USER_ERROR("A0001","用户端错误"),
/** /**
* 二级宏观错误码用户注册错误 * 二级宏观错误码用户注册错误
*/ * */
USER_REGISTER_ERROR("A0100", "用户注册错误"), USER_REGISTER_ERROR("A0100","用户注册错误"),
/** /**
* 用户未同意隐私协议 * 用户未同意隐私协议
*/ * */
USER_NO_AGREE_PRIVATE_ERROR("A0101", "用户未同意隐私协议"), USER_NO_AGREE_PRIVATE_ERROR("A0101","用户未同意隐私协议"),
/** /**
* 注册国家或地区受限 * 注册国家或地区受限
*/ * */
USER_REGISTER_AREA_LIMIT_ERROR("A0102", "注册国家或地区受限"), USER_REGISTER_AREA_LIMIT_ERROR("A0102","注册国家或地区受限"),
/** /**
* 用户验证码错误 * 用户验证码错误
*/ * */
USER_VERIFY_CODE_ERROR("A0240", "用户验证码错误"), USER_VERIFY_CODE_ERROR("A0240","用户验证码错误"),
/** /**
* 用户名已存在 * 用户名已存在
*/ * */
USER_NAME_EXIST("A0111", "用户名已存在"), USER_NAME_EXIST("A0111","用户名已存在"),
/** /**
* 用户账号不存在 * 用户账号不存在
*/ * */
USER_ACCOUNT_NOT_EXIST("A0201", "用户账号不存在"), USER_ACCOUNT_NOT_EXIST("A0201","用户账号不存在"),
/** /**
* 用户密码错误 * 用户密码错误
*/ * */
USER_PASSWORD_ERROR("A0210", "用户密码错误"), USER_PASSWORD_ERROR("A0210","用户密码错误"),
/** /**
* 二级宏观错误码用户请求参数错误 * 二级宏观错误码用户请求参数错误
*/ * */
USER_REQUEST_PARAM_ERROR("A0400", "用户请求参数错误"), USER_REQUEST_PARAM_ERROR("A0400","用户请求参数错误"),
/** /**
* 用户登录已过期 * 用户登录已过期
*/ * */
USER_LOGIN_EXPIRED("A0230", "用户登录已过期"), USER_LOGIN_EXPIRED("A0230","用户登录已过期"),
/** /**
* 访问未授权 * 访问未授权
*/ * */
USER_UN_AUTH("A0301", "访问未授权"), USER_UN_AUTH("A0301","访问未授权"),
/**
* 用户请求服务异常
*/
USER_REQ_EXCEPTION("A0500", "用户请求服务异常"),
/**
* 请求超出限制
*/
USER_REQ_MANY("A0501", "请求超出限制"),
/** /**
* 用户评论异常 * 用户评论异常
*/ * */
USER_COMMENT("A2000", "用户评论异常"), USER_COMMENT("A2000","用户评论异常"),
/** /**
* 用户评论异常 * 用户评论异常
*/ * */
USER_COMMENTED("A2001", "用户已发表评论"), 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_ERROR("B0001","系统执行出错"),
/** /**
* 二级宏观错误码系统执行超时 * 二级宏观错误码系统执行超时
*/ * */
SYSTEM_TIMEOUT_ERROR("B0100", "系统执行超时"), SYSTEM_TIMEOUT_ERROR("B0100","系统执行超时"),
/** /**
* 一级宏观错误码调用第三方服务出错 * 一级宏观错误码调用第三方服务出错
*/ * */
THIRD_SERVICE_ERROR("C0001", "调用第三方服务出错"), THIRD_SERVICE_ERROR("C0001","调用第三方服务出错"),
/** /**
* 一级宏观错误码中间件服务出错 * 一级宏观错误码中间件服务出错
*/ * */
MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"); MIDDLEWARE_SERVICE_ERROR("C0100","中间件服务出错")
;
/** /**
* 错误码 * 错误码
*/ * */
private final String code; private String code;
/** /**
* 中文描述 * 中文描述
*/ * */
private final String message; private String message;
} }

View File

@ -3,12 +3,9 @@ package io.github.xxyopen.novel.core.common.exception;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum; import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.common.resp.RestResp;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
/** /**
* 通用的异常处理器 * 通用的异常处理器
@ -20,39 +17,30 @@ import org.springframework.web.servlet.resource.NoResourceFoundException;
@RestControllerAdvice @RestControllerAdvice
public class CommonExceptionHandler { public class CommonExceptionHandler {
/**
* 处理404异常
*/
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handlerNotFound() {
return "404";
}
/** /**
* 处理数据校验异常 * 处理数据校验异常
*/ * */
@ExceptionHandler(BindException.class) @ExceptionHandler(BindException.class)
public RestResp<Void> handlerBindException(BindException e) { public RestResp<Void> handlerBindException(BindException e){
log.error(e.getMessage(), e); log.error(e.getMessage(),e);
return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR); return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR);
} }
/** /**
* 处理业务异常 * 处理业务异常
*/ * */
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
public RestResp<Void> handlerBusinessException(BusinessException e) { public RestResp<Void> handlerBusinessException(BusinessException e){
log.error(e.getMessage(), e); log.error(e.getMessage(),e);
return RestResp.fail(e.getErrorCodeEnum()); return RestResp.fail(e.getErrorCodeEnum());
} }
/** /**
* 处理系统异常 * 处理系统异常
*/ * */
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public RestResp<Void> handlerException(Exception e) { public RestResp<Void> handlerException(Exception e){
log.error(e.getMessage(), e); log.error(e.getMessage(),e);
return RestResp.error(); return RestResp.error();
} }

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.core.common.req; package io.github.xxyopen.novel.core.common.req;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.Data; import lombok.Data;
/** /**
@ -14,20 +13,18 @@ public class PageReqDto {
/** /**
* 请求页码默认第 1 * 请求页码默认第 1
*/ * */
@Parameter(description = "请求页码,默认第 1 页")
private int pageNum = 1; private int pageNum = 1;
/** /**
* 每页大小默认每页 10 * 每页大小默认每页 10
*/ * */
@Parameter(description = "每页大小,默认每页 10 条")
private int pageSize = 10; private int pageSize = 10;
/** /**
* 是否查询所有默认不查所有 true pageNum pageSize 无效 * 是否查询所有默认不查所有
*/ * true pageNum pageSize 无效
@Parameter(hidden = true) * */
private boolean fetchAll = false; private boolean fetchAll = false;
} }

View File

@ -1,8 +1,9 @@
package io.github.xxyopen.novel.core.common.resp; package io.github.xxyopen.novel.core.common.resp;
import java.util.List;
import lombok.Getter; import lombok.Getter;
import java.util.List;
/** /**
* 分页响应数据格式封装 * 分页响应数据格式封装
* *
@ -33,7 +34,8 @@ public class PageRespDto<T> {
private final List<? extends T> list; private final List<? extends T> list;
/** /**
* 该构造函数用于通用分页查询的场景 接收普通分页数据和普通集合 * 该构造函数用于通用分页查询的场景
* 接收普通分页数据和普通集合
*/ */
public PageRespDto(long pageNum, long pageSize, long total, List<T> list) { public PageRespDto(long pageNum, long pageSize, long total, List<T> list) {
this.pageNum = pageNum; this.pageNum = pageNum;
@ -48,7 +50,7 @@ public class PageRespDto<T> {
/** /**
* 获取分页数 * 获取分页数
*/ * */
public long getPages() { public long getPages() {
if (this.pageSize == 0L) { if (this.pageSize == 0L) {
return 0L; return 0L;
@ -57,6 +59,7 @@ public class PageRespDto<T> {
if (this.total % this.pageSize != 0L) { if (this.total % this.pageSize != 0L) {
++pages; ++pages;
} }
return pages; return pages;
} }
} }

View File

@ -1,7 +1,6 @@
package io.github.xxyopen.novel.core.common.resp; package io.github.xxyopen.novel.core.common.resp;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum; import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter; import lombok.Getter;
import java.util.Objects; import java.util.Objects;
@ -18,19 +17,16 @@ public class RestResp<T> {
/** /**
* 响应码 * 响应码
*/ */
@Schema(description = "错误码00000-没有错误")
private String code; private String code;
/** /**
* 响应消息 * 响应消息
*/ */
@Schema(description = "响应消息")
private String message; private String message;
/** /**
* 响应数据 * 响应数据
*/ */
@Schema(description = "响应数据")
private T data; private T data;
private RestResp() { private RestResp() {

View File

@ -1,15 +1,14 @@
package io.github.xxyopen.novel.core.common.util; package io.github.xxyopen.novel.core.common.util;
import java.awt.Color; import lombok.experimental.UtilityClass;
import java.awt.Font;
import java.awt.Graphics; import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Base64; import java.util.Base64;
import java.util.Random; import java.util.Random;
import javax.imageio.ImageIO;
import lombok.experimental.UtilityClass;
/** /**
* 图片验证码工具类 * 图片验证码工具类

View File

@ -16,7 +16,6 @@ public class IpUtils {
/** /**
* 获取真实IP * 获取真实IP
*
* @return 真实IP * @return 真实IP
*/ */
public String getRealIp(HttpServletRequest request) { public String getRealIp(HttpServletRequest request) {

View File

@ -1,47 +0,0 @@
package io.github.xxyopen.novel.core.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
/**
* Ai 相关配置
*
* @author xiongxiaoyang
* @date 2025/2/19
*/
@Configuration
@Slf4j
public class AiConfig {
/**
* 目的配置自定义的 RestClientBuilder 对象
* <p>
* 原因Spring AI 框架的 ChatClient 内部通过 RestClientSpring Framework 6 Spring Boot 3 中引入 发起 HTTP REST 请求与远程的大模型服务进行通信
* 如果项目中没有配置自定义的 RestClientBuilder 对象 那么在 RestClient 的自动配置类 org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
* 中配置的 RestClientBuilder 对象会使用 Spring 容器中提供的 HttpMessageConverters 由于本项目中配置了 spring.jackson.generator.write-numbers-as-strings
* = true 所以 Spring 容器中的 HttpMessageConverters RestClient 发起 HTTP REST 请求转换 Java 对象为 JSON 字符串时会自动将 Number 类型的
* Java 对象属性转换为字符串而导致请求参数错误
* <p>
* 示例"temperature": 0.7 ="temperature": "0.7"
* {"code":20015,"message":"The parameter is invalid. Please check again.","data":null}
*/
@Bean
public RestClient.Builder restClientBuilder() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// 连接超时时间
factory.setConnectTimeout(5000);
// 读取超时时间
factory.setReadTimeout(60000);
return RestClient.builder().requestFactory(factory);
}
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
return chatClientBuilder.build();
}
}

View File

@ -1,44 +0,0 @@
package io.github.xxyopen.novel.core.config;
import io.github.xxyopen.novel.core.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());
}
}

View File

@ -2,11 +2,6 @@ package io.github.xxyopen.novel.core.config;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import io.github.xxyopen.novel.core.constant.CacheConsts; import io.github.xxyopen.novel.core.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.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager; import org.springframework.cache.support.SimpleCacheManager;
@ -18,6 +13,12 @@ import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** /**
* 缓存配置类 * 缓存配置类
* *
@ -36,11 +37,9 @@ public class CacheConfig {
SimpleCacheManager cacheManager = new SimpleCacheManager(); SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>(CacheConsts.CacheEnum.values().length); List<CaffeineCache> caches = new ArrayList<>(CacheConsts.CacheEnum.values().length);
// 类型推断 var 非常适合 for 循环JDK 10 引入JDK 11 改进 for (CacheConsts.CacheEnum c : CacheConsts.CacheEnum.values()) {
for (var c : CacheConsts.CacheEnum.values()) {
if (c.isLocal()) { if (c.isLocal()) {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder().recordStats() Caffeine<Object, Object> caffeine = Caffeine.newBuilder().recordStats().maximumSize(c.getMaxSize());
.maximumSize(c.getMaxSize());
if (c.getTtl() > 0) { if (c.getTtl() > 0) {
caffeine.expireAfterWrite(Duration.ofSeconds(c.getTtl())); caffeine.expireAfterWrite(Duration.ofSeconds(c.getTtl()));
} }
@ -57,32 +56,25 @@ public class CacheConfig {
*/ */
@Bean @Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) { public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter( RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
connectionFactory);
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues().prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX); .disableCachingNullValues().prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX);
Map<String, RedisCacheConfiguration> cacheMap = new LinkedHashMap<>( Map<String, RedisCacheConfiguration> cacheMap = new LinkedHashMap<>(CacheConsts.CacheEnum.values().length);
CacheConsts.CacheEnum.values().length); for (CacheConsts.CacheEnum c : CacheConsts.CacheEnum.values()) {
// 类型推断 var 非常适合 for 循环JDK 10 引入JDK 11 改进
for (var c : CacheConsts.CacheEnum.values()) {
if (c.isRemote()) { if (c.isRemote()) {
if (c.getTtl() > 0) { if (c.getTtl() > 0) {
cacheMap.put(c.getName(), cacheMap.put(c.getName(), RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues() .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX).entryTtl(Duration.ofSeconds(c.getTtl())));
.prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX)
.entryTtl(Duration.ofSeconds(c.getTtl())));
} else { } else {
cacheMap.put(c.getName(), cacheMap.put(c.getName(), RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
.prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX)); .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX));
} }
} }
} }
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter, RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig, cacheMap);
defaultCacheConfig, cacheMap);
redisCacheManager.setTransactionAware(true); redisCacheManager.setTransactionAware(true);
redisCacheManager.initializeCaches(); redisCacheManager.initializeCaches();
return redisCacheManager; return redisCacheManager;

View File

@ -25,7 +25,7 @@ public class CorsConfig {
public CorsFilter corsFilter() { public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration(); CorsConfiguration config = new CorsConfiguration();
// 允许的域,不要写*否则cookie就无法使用了 // 允许的域,不要写*否则cookie就无法使用了
for (String allowOrigin : corsProperties.allowOrigins()) { for (String allowOrigin : corsProperties.getAllowOrigins()) {
config.addAllowedOrigin(allowOrigin); config.addAllowedOrigin(allowOrigin);
} }
// 允许的头信息 // 允许的头信息
@ -37,7 +37,7 @@ public class CorsConfig {
UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource(); UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
// 添加映射路径拦截一切请求 // 添加映射路径拦截一切请求
configurationSource.registerCorsConfiguration("/**", config); configurationSource.registerCorsConfiguration("/**",config);
return new CorsFilter(configurationSource); return new CorsFilter(configurationSource);
} }

View File

@ -1,5 +1,6 @@
package io.github.xxyopen.novel.core.config; package io.github.xxyopen.novel.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List; import java.util.List;
@ -11,6 +12,11 @@ import java.util.List;
* @date 2022/5/17 * @date 2022/5/17
*/ */
@ConfigurationProperties(prefix = "novel.cors") @ConfigurationProperties(prefix = "novel.cors")
public record CorsProperties(List<String> allowOrigins) { @Data
public class CorsProperties {
/**
* 允许跨域的域名
* */
private List<String> allowOrigins;
} }

View File

@ -1,73 +0,0 @@
package io.github.xxyopen.novel.core.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.elasticsearch.RestClientBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
/**
* Elasticsearch 相关配置
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Configuration
@Slf4j
public class EsConfig {
/**
* fix `sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
* unable to find valid certification path to requested target`
*/
@ConditionalOnProperty(value = "spring.elasticsearch.ssl.verification-mode", havingValue = "none")
@Bean
RestClient elasticsearchRestClient(RestClientBuilder restClientBuilder,
ObjectProvider<RestClientBuilderCustomizer> builderCustomizers) {
restClientBuilder.setHttpClientConfigCallback((HttpAsyncClientBuilder clientBuilder) -> {
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}};
SSLContext sc = null;
try {
sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new SecureRandom());
} catch (KeyManagementException | NoSuchAlgorithmException e) {
log.error("Elasticsearch RestClient 配置失败!", e);
}
assert sc != null;
clientBuilder.setSSLContext(sc);
clientBuilder.setSSLHostnameVerifier((hostname, session) -> true);
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(clientBuilder));
return clientBuilder;
});
return restClientBuilder.build();
}
}

View File

@ -1,37 +0,0 @@
package io.github.xxyopen.novel.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zalando.logbook.Logbook;
import static org.zalando.logbook.core.Conditions.*;
/**
* Logbook 配置
*
* @author xiongxiaoyang
* @date 2024/9/13
*/
@Configuration
public class LogbookConfig {
@Bean
public Logbook logbook() {
return Logbook.builder()
.condition(exclude(
// 忽略 OPTIONS 请求
requestWithMethod("OPTIONS"),
// 忽略 /actuator 以及其子路径Spring Boot Actuator 提供的端点的请求
requestTo("/actuator/**"),
// 忽略 Swagger 文档路径
requestTo("/swagger-ui/**"),
requestTo("/v3/api-docs/**"),
// 忽略二进制文件请求
contentType("application/octet-stream"),
// 忽略文件上传请求
contentType("multipart/form-data")
))
.build();
}
}

View File

@ -1,14 +0,0 @@
package io.github.xxyopen.novel.core.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* mail 配置属性
*
* @author xiongxiaoyang
* @date 2023/3/25
*/
@ConfigurationProperties(prefix = "spring.mail")
public record MailProperties(String nickname, String username) {
}

View File

@ -1,25 +0,0 @@
package io.github.xxyopen.novel.core.config;
import io.github.xxyopen.novel.core.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 {
}

View File

@ -1,40 +0,0 @@
package io.github.xxyopen.novel.core.config;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.driver.api.yaml.YamlShardingSphereDataSourceFactory;
import org.apache.shardingsphere.infra.url.core.ShardingSphereURL;
import org.apache.shardingsphere.infra.url.core.ShardingSphereURLLoadEngine;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* ShardingSphere 配置类控制是否开启 ShardingSphere
*
* @author xiongxiaoyang
* @date 2023/12/21
*/
@Configuration
@ConditionalOnProperty(
prefix = "spring.shardingsphere",
name = {"enabled"},
havingValue = "true"
)
@Slf4j
public class ShardingSphereConfiguration {
private static final String URL = "classpath:shardingsphere-jdbc.yml";
@Bean
@SneakyThrows
public DataSource shardingSphereDataSource() {
log.info(">>>>>>>>>>> shardingSphereDataSource init.");
ShardingSphereURLLoadEngine urlLoadEngine = new ShardingSphereURLLoadEngine(
ShardingSphereURL.parse(URL));
return YamlShardingSphereDataSourceFactory.createDataSource(urlLoadEngine.loadContent());
}
}

View File

@ -1,19 +1,16 @@
package io.github.xxyopen.novel.core.config; package io.github.xxyopen.novel.core.config;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.core.interceptor.AuthInterceptor; import io.github.xxyopen.novel.core.interceptor.AuthInterceptor;
import io.github.xxyopen.novel.core.interceptor.FileInterceptor;
import io.github.xxyopen.novel.core.interceptor.FlowLimitInterceptor;
import io.github.xxyopen.novel.core.interceptor.TokenParseInterceptor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/** /**
* Spring Web Mvc 相关配置不要加 @EnableWebMvc 注解否则会导致 jackson 的全局配置失效因为 @EnableWebMvc 注解会导致 * Spring Web Mvc 相关配置
* WebMvcAutoConfiguration 自动配置失效 * 不要加 @EnableWebMvc 注解否则会导致 jackson 的全局配置失效
* 类上添加 @EnableWebMvc 会导致 WebMvcAutoConfiguration 中的自动配置全部失效
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/18 * @date 2022/5/18
@ -22,46 +19,21 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@RequiredArgsConstructor @RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer { public class WebConfig implements WebMvcConfigurer {
private final FlowLimitInterceptor flowLimitInterceptor; private final AuthInterceptor frontAuthInterceptor;
private final AuthInterceptor authInterceptor;
private final FileInterceptor fileInterceptor;
private final TokenParseInterceptor tokenParseInterceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(frontAuthInterceptor)
// 流量限制拦截器
registry.addInterceptor(flowLimitInterceptor)
.addPathPatterns("/**")
.order(0);
// 文件访问拦截
registry.addInterceptor(fileInterceptor)
.addPathPatterns(SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY + "**")
.order(1);
// 权限认证拦截
registry.addInterceptor(authInterceptor)
// 拦截会员中心相关请求接口 // 拦截会员中心相关请求接口
.addPathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/**", .addPathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/**"
// 拦截作家后台相关请求接口 // 拦截作家后台相关请求接口
ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/**", , ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/**"
// 拦截平台后台相关请求接口 // 拦截平台后台相关请求接口
ApiRouterConsts.API_ADMIN_URL_PREFIX + "/**") , ApiRouterConsts.API_ADMIN_URL_PREFIX + "/**")
// 放行登录注册相关请求接口 // 放行登录注册相关请求接口
.excludePathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/register", .excludePathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/register"
ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/login", , ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/login"
ApiRouterConsts.API_ADMIN_URL_PREFIX + "/login") , ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/register"
.order(2); ,ApiRouterConsts.API_ADMIN_URL_PREFIX + "/login");
// Token 解析拦截器
registry.addInterceptor(tokenParseInterceptor)
// 拦截小说内容查询接口需要解析 token 以判断该用户是否有权阅读该章节付费章节是否已购买
.addPathPatterns(ApiRouterConsts.API_FRONT_BOOK_URL_PREFIX + "/content/*")
.order(3);
} }
} }

View File

@ -1,8 +1,10 @@
package io.github.xxyopen.novel.core.config; package io.github.xxyopen.novel.core.config;
import java.util.List; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/** /**
* Xss 过滤配置属性 * Xss 过滤配置属性
* *
@ -10,6 +12,17 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @date 2022/5/17 * @date 2022/5/17
*/ */
@ConfigurationProperties(prefix = "novel.xss") @ConfigurationProperties(prefix = "novel.xss")
public record XssProperties(Boolean enabled, List<String> excludes) { @Data
public class XssProperties {
/**
* 过滤开关
* */
private Boolean enabled;
/**
* 排除链接
* */
private List<String> excludes;
} }

View File

@ -1,44 +0,0 @@
package io.github.xxyopen.novel.core.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* XXL-JOB 配置类
*
* @author xiongxiaoyang
* @date 2022/5/31
*/
@Configuration
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true")
@Slf4j
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setLogPath(logPath);
return xxlJobSpringExecutor;
}
}

View File

@ -1,33 +0,0 @@
package io.github.xxyopen.novel.core.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";
}
}

View File

@ -34,39 +34,29 @@ public class ApiRouterConsts {
/** /**
* 首页模块请求路径前缀 * 首页模块请求路径前缀
*/ * */
public static final String HOME_URL_PREFIX = "/home"; public static final String HOME_URL_PREFIX = "/home";
/** /**
* 首页模块请求路径前缀 * 首页模块请求路径前缀
*/ * */
public static final String NEWS_URL_PREFIX = "/news"; public static final String NEWS_URL_PREFIX = "/news";
/** /**
* 小说模块请求路径前缀 * 小说模块请求路径前缀
*/ * */
public static final String BOOK_URL_PREFIX = "/book"; public static final String BOOK_URL_PREFIX = "/book";
/** /**
* 会员模块请求路径前缀 * 会员模块请求路径前缀
*/ * */
public static final String USER_URL_PREFIX = "/user"; public static final String USER_URL_PREFIX = "/user";
/** /**
* 资源图片/视频/文档模块请求路径前缀 * 资源图片/视频/文档模块请求路径前缀
*/ * */
public static final String RESOURCE_URL_PREFIX = "/resource"; public static final String RESOURCE_URL_PREFIX = "/resource";
/**
* 搜索模块请求路径前缀
*/
public static final String SEARCH_URL_PREFIX = "/search";
/**
* AI模块请求路径前缀
*/
public static final String AI_URL_PREFIX = "/ai";
/** /**
* 前台门户首页API请求路径前缀 * 前台门户首页API请求路径前缀
*/ */
@ -90,19 +80,6 @@ public class ApiRouterConsts {
/** /**
* 前台门户资源图片/视频/文档相关API请求路径前缀 * 前台门户资源图片/视频/文档相关API请求路径前缀
*/ */
public static final String API_FRONT_RESOURCE_URL_PREFIX = public static final String API_FRONT_RESOURCE_URL_PREFIX = API_FRONT_URL_PREFIX + 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;
/**
* 作家后台AI相关API请求路径前缀
*/
public static final String API_AUTHOR_AI_URL_PREFIX = API_AUTHOR_URL_PREFIX + AI_URL_PREFIX;
} }

View File

@ -56,7 +56,7 @@ public class CacheConsts {
/** /**
* 小说分类列表缓存 * 小说分类列表缓存
*/ * */
public static final String BOOK_CATEGORY_LIST_CACHE_NAME = "bookCategoryListCache"; public static final String BOOK_CATEGORY_LIST_CACHE_NAME = "bookCategoryListCache";
/** /**
@ -76,14 +76,13 @@ public class CacheConsts {
/** /**
* 最近更新小说ID列表缓存 * 最近更新小说ID列表缓存
*/ * */
public static final String LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME = "lastUpdateBookIdListCache"; public static final String LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME = "lastUpdateBookIdListCache";
/** /**
* 图片验证码缓存 KEY * 图片验证码缓存 KEY
*/ * */
public static final String IMG_VERIFY_CODE_CACHE_KEY = public static final String IMG_VERIFY_CODE_CACHE_KEY = REDIS_CACHE_PREFIX + "imgVerifyCodeCache::";
REDIS_CACHE_PREFIX + "imgVerifyCodeCache::";
/** /**
* 用户信息缓存 * 用户信息缓存
@ -112,19 +111,19 @@ public class CacheConsts {
HOME_FRIEND_LINK_CACHE(2, HOME_FRIEND_LINK_CACHE_NAME, 0, 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_CATEGORY_LIST_CACHE(0,BOOK_CATEGORY_LIST_CACHE_NAME,0,2),
BOOK_INFO_CACHE(0, BOOK_INFO_CACHE_NAME, 60 * 60 * 18, 500), 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_CHAPTER_CACHE(0,BOOK_CHAPTER_CACHE_NAME,60 * 60 * 6,5000),
BOOK_CONTENT_CACHE(2, BOOK_CONTENT_CACHE_NAME, 60 * 60 * 12, 3000), 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), 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), USER_INFO_CACHE(2,USER_INFO_CACHE_NAME,60 * 60 * 24, 10000),
AUTHOR_INFO_CACHE(2, AUTHOR_INFO_CACHE_NAME, 60 * 60 * 48, 1000); AUTHOR_INFO_CACHE(2,AUTHOR_INFO_CACHE_NAME,60 * 60 * 48, 1000);
/** /**
* 缓存类型 0-本地 1-本地和远程 2-远程 * 缓存类型 0-本地 1-本地和远程 2-远程

View File

@ -10,6 +10,7 @@ import lombok.Getter;
*/ */
public class DatabaseConsts { public class DatabaseConsts {
/** /**
* 用户信息表 * 用户信息表
*/ */
@ -88,14 +89,8 @@ public class DatabaseConsts {
public static final String COLUMN_CATEGORY_ID = "category_id"; 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_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 final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
} }

View File

@ -1,116 +0,0 @@
package io.github.xxyopen.novel.core.constant;
/**
* elasticsearch 相关常量
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
public class EsConsts {
private EsConsts() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
/**
* 小说索引
*/
public static class BookIndex {
private BookIndex() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
}
/**
* 索引名
*/
public static final String INDEX_NAME = "book";
/**
* id
*/
public static final String FIELD_ID = "id";
/**
* 作品方向;0-男频 1-女频
*/
public static final String FIELD_WORK_DIRECTION = "workDirection";
/**
* 类别ID
*/
public static final String FIELD_CATEGORY_ID = "categoryId";
/**
* 类别名
*/
public static final String FIELD_CATEGORY_NAME = "categoryName";
/**
* 小说名
*/
public static final String FIELD_BOOK_NAME = "bookName";
/**
* 作家id
*/
public static final String FIELD_AUTHOR_ID = "authorId";
/**
* 作家名
*/
public static final String FIELD_AUTHOR_NAME = "authorName";
/**
* 书籍描述
*/
public static final String FIELD_BOOK_DESC = "bookDesc";
/**
* 评分;总分:10 真实评分 = score/10
*/
public static final String FIELD_SCORE = "score";
/**
* 书籍状态;0-连载中 1-已完结
*/
public static final String FIELD_BOOK_STATUS = "bookStatus";
/**
* 点击量
*/
public static final String FIELD_VISIT_COUNT = "visitCount";
/**
* 总字数
*/
public static final String FIELD_WORD_COUNT = "wordCount";
/**
* 评论数
*/
public static final String FIELD_COMMENT_COUNT = "commentCount";
/**
* 最新章节ID
*/
public static final String FIELD_LAST_CHAPTER_ID = "lastChapterId";
/**
* 最新章节名
*/
public static final String FIELD_LAST_CHAPTER_NAME = "lastChapterName";
/**
* 最新章节更新时间
*/
public static final String FIELD_LAST_CHAPTER_UPDATE_TIME = "lastChapterUpdateTime";
/**
* 是否收费;1-收费 0-免费
*/
public static final String FIELD_IS_VIP = "isVip";
}
}

View File

@ -1,25 +0,0 @@
package io.github.xxyopen.novel.core.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";
}

View File

@ -14,32 +14,27 @@ public class SystemConfigConsts {
/** /**
* Http 请求认证 Header * Http 请求认证 Header
*/ * */
public static final String HTTP_AUTH_HEADER_NAME = "Authorization"; public static final String HTTP_AUTH_HEADER_NAME = "Authorization";
/** /**
* 前台门户系统标识 * 前台门户系统标识
*/ * */
public static final String NOVEL_FRONT_KEY = "front"; public static final String NOVEL_FRONT_KEY = "front";
/** /**
* 作家管理系统标识 * 作家管理系统标识
*/ * */
public static final String NOVEL_AUTHOR_KEY = "author"; public static final String NOVEL_AUTHOR_KEY = "author";
/** /**
* 后台管理系统标识 * 后台管理系统标识
*/ * */
public static final String NOVEL_ADMIN_KEY = "admin"; 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"; public static final String CONST_INSTANCE_EXCEPTION_MSG = "Constant class";
} }

View File

@ -3,22 +3,18 @@ package io.github.xxyopen.novel.core.filter;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import io.github.xxyopen.novel.core.config.XssProperties; import io.github.xxyopen.novel.core.config.XssProperties;
import io.github.xxyopen.novel.core.wrapper.XssHttpServletRequestWrapper; import io.github.xxyopen.novel.core.wrapper.XssHttpServletRequestWrapper;
import jakarta.servlet.Filter; import jakarta.servlet.*;
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.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* 防止 XSS 攻击的过滤器 * 防止 XSS 攻击的过滤器
* *
@ -26,12 +22,13 @@ import org.springframework.stereotype.Component;
* @date 2022/5/17 * @date 2022/5/17
*/ */
@Component @Component
@ConditionalOnProperty(value = "novel.xss.enabled", havingValue = "true") @ConditionalOnProperty(value = "novel.xss.enabled",havingValue = "true")
@WebFilter(urlPatterns = "/*", filterName = "xssFilter") @WebFilter(urlPatterns = "/*", filterName = "xssFilter")
@EnableConfigurationProperties(value = {XssProperties.class}) @EnableConfigurationProperties(value = {XssProperties.class})
@RequiredArgsConstructor @RequiredArgsConstructor
public class XssFilter implements Filter { public class XssFilter implements Filter {
private final XssProperties xssProperties; private final XssProperties xssProperties;
@Override @Override
@ -40,24 +37,22 @@ public class XssFilter implements Filter {
} }
@Override @Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletRequest req = (HttpServletRequest) servletRequest;
if (handleExcludeUrl(req)) { if (handleExcludeUrl(req)) {
filterChain.doFilter(servletRequest, servletResponse); filterChain.doFilter(servletRequest, servletResponse);
return; return;
} }
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper( XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) servletRequest);
(HttpServletRequest) servletRequest);
filterChain.doFilter(xssRequest, servletResponse); filterChain.doFilter(xssRequest, servletResponse);
} }
private boolean handleExcludeUrl(HttpServletRequest request) { private boolean handleExcludeUrl(HttpServletRequest request) {
if (CollectionUtils.isEmpty(xssProperties.excludes())) { if (CollectionUtils.isEmpty(xssProperties.getExcludes())) {
return false; return false;
} }
String url = request.getServletPath(); String url = request.getServletPath();
for (String pattern : xssProperties.excludes()) { for (String pattern : xssProperties.getExcludes()) {
Pattern p = Pattern.compile("^" + pattern); Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(url); Matcher m = p.matcher(url);
if (m.find()) { if (m.find()) {

View File

@ -19,7 +19,8 @@ import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
/** /**
* 认证授权 拦截器为了注入其它的 Spring beans需要通过 @Component 注解将该拦截器注册到 Spring 上下文 * 认证 拦截器
* 为了注入其它的 Spring beans需要通过 @Component 注解将该拦截器注册到 Spring 上下文
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/18 * @date 2022/5/18
@ -28,17 +29,13 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor { public class AuthInterceptor implements HandlerInterceptor {
private final Map<String, AuthStrategy> authStrategy; private final Map<String,AuthStrategy> authStrategy;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
/**
* handle 执行前调用
*/
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object handler) throws Exception {
// 获取登录 JWT // 获取登录 JWT
String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME); String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);
@ -46,44 +43,28 @@ public class AuthInterceptor implements HandlerInterceptor {
String requestUri = request.getRequestURI(); String requestUri = request.getRequestURI();
// 根据请求的 URI 得到认证策略 // 根据请求的 URI 得到认证策略
String subUri = requestUri.substring(ApiRouterConsts.API_URL_PREFIX.length() + 1); String authStrategyName = requestUri.substring(ApiRouterConsts.API_URL_PREFIX.length() + 1);
String systemName = subUri.substring(0, subUri.indexOf("/")); authStrategyName = authStrategyName.substring(0,authStrategyName.indexOf("/"));
String authStrategyName = String.format("%sAuthStrategy", systemName); authStrategyName = String.format("%sAuthStrategy",authStrategyName);
// 开始认证 // 开始认证
try { try {
authStrategy.get(authStrategyName).auth(token, requestUri); authStrategy.get(authStrategyName).auth(token);
return HandlerInterceptor.super.preHandle(request, response, handler); return HandlerInterceptor.super.preHandle(request, response, handler);
} catch (BusinessException exception) { }catch (BusinessException exception){
// 认证失败 // 认证失败
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write( response.getWriter().write(objectMapper.writeValueAsString(RestResp.fail(exception.getErrorCodeEnum())));
objectMapper.writeValueAsString(RestResp.fail(exception.getErrorCodeEnum())));
return false; return false;
} }
} }
/**
* handler 执行后调用出现异常不调用
*/
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
@Override @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* DispatcherServlet 完全处理完请求后调用出现异常照常调用
*/
@SuppressWarnings("NullableProblems")
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 清理当前线程保存的用户数据 // 清理当前线程保存的用户数据
UserHolder.clear(); UserHolder.clear();
HandlerInterceptor.super.afterCompletion(request, response, handler, ex); HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
} }
} }

View File

@ -1,44 +0,0 @@
package io.github.xxyopen.novel.core.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 文件 拦截器
*
* @author xiongxiaoyang
* @date 2022/5/22
*/
@Component
@RequiredArgsConstructor
public class FileInterceptor implements HandlerInterceptor {
@Value("${novel.file.upload.path}")
private String fileUploadPath;
@SuppressWarnings("NullableProblems")
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 获取请求的 URI
String requestUri = request.getRequestURI();
// 缓存10天
response.setDateHeader("expires", System.currentTimeMillis() + 60 * 60 * 24 * 10 * 1000);
try (OutputStream out = response.getOutputStream(); InputStream input = new FileInputStream(
fileUploadPath + requestUri)) {
byte[] b = new byte[4096];
for (int n; (n = input.read(b)) != -1; ) {
out.write(b, 0, n);
}
}
return false;
}
}

View File

@ -1,98 +0,0 @@
package io.github.xxyopen.novel.core.interceptor;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.common.util.IpUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 流量限制 拦截器实现接口防刷和限流
*
* @author xiongxiaoyang
* @date 2022/6/1
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class FlowLimitInterceptor implements HandlerInterceptor {
private final ObjectMapper objectMapper;
/**
* novel 项目所有的资源
*/
private static final String NOVEL_RESOURCE = "novelResource";
static {
// 接口限流规则所有的请求限制每秒最多只能通过 2000 超出限制匀速排队
List<FlowRule> rules = new ArrayList<>();
FlowRule rule1 = new FlowRule();
rule1.setResource(NOVEL_RESOURCE);
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
// Set limit QPS to 2000.
rule1.setCount(2000);
rule1.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
rules.add(rule1);
FlowRuleManager.loadRules(rules);
// 接口防刷规则 1所有的请求限制每个 IP 每秒最多只能通过 50 超出限制直接拒绝
ParamFlowRule rule2 = new ParamFlowRule(NOVEL_RESOURCE)
.setParamIdx(0)
.setCount(50);
// 接口防刷规则 2所有的请求限制每个 IP 每分钟最多只能通过 1000 超出限制直接拒绝
ParamFlowRule rule3 = new ParamFlowRule(NOVEL_RESOURCE)
.setParamIdx(0)
.setCount(1000)
.setDurationInSec(60);
ParamFlowRuleManager.loadRules(Arrays.asList(rule2, rule3));
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String ip = IpUtils.getRealIp(request);
Entry entry = null;
try {
// 若需要配置例外项则传入的参数只支持基本类型
// EntryType 代表流量类型其中系统规则只对 IN 类型的埋点生效
// count 大多数情况都填 1代表统计为一次调用
entry = SphU.entry(NOVEL_RESOURCE, EntryType.IN, 1, ip);
// Your logic here.
return HandlerInterceptor.super.preHandle(request, response, handler);
} catch (BlockException ex) {
// Handle request rejection.
log.info("IP:{}被限流了!", ip);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter()
.write(objectMapper.writeValueAsString(RestResp.fail(ErrorCodeEnum.USER_REQ_MANY)));
} finally {
// 注意exit 的时候也一定要带上对应的参数否则可能会有统计错误
if (entry != null) {
entry.exit(1, ip);
}
}
return false;
}
}

View File

@ -1,50 +0,0 @@
package io.github.xxyopen.novel.core.interceptor;
import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.core.util.JwtUtils;
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 {
private final JwtUtils jwtUtils;
@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);
}
}

View File

@ -1,32 +0,0 @@
package io.github.xxyopen.novel.core.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("<", "&lt;")
.replace(">", "&gt;");
}
}
}

View File

@ -3,6 +3,7 @@ package io.github.xxyopen.novel.core.json.serializer;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException; import java.io.IOException;
/** /**
@ -14,9 +15,8 @@ import java.io.IOException;
public class UsernameSerializer extends JsonSerializer<String> { public class UsernameSerializer extends JsonSerializer<String> {
@Override @Override
public void serialize(String s, JsonGenerator jsonGenerator, public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(s.substring(0,4) + "****" + s.substring(8));
jsonGenerator.writeString(s.substring(0, 4) + "****" + s.substring(8));
} }
} }

View File

@ -1,49 +0,0 @@
package io.github.xxyopen.novel.core.listener;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.IndexResponse;
import io.github.xxyopen.novel.core.constant.AmqpConsts;
import io.github.xxyopen.novel.core.constant.EsConsts;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.es.EsBookDto;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* Rabbit 队列监听器
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
@Component
@ConditionalOnProperty(prefix = "spring", name = {"elasticsearch.enabled",
"amqp.enabled"}, havingValue = "true")
@RequiredArgsConstructor
@Slf4j
public class RabbitQueueListener {
private final BookInfoMapper bookInfoMapper;
private final ElasticsearchClient esClient;
/**
* 监听小说信息改变的 ES 更新队列更新最新小说信息到 ES
*/
@RabbitListener(queues = AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE)
@SneakyThrows
public void updateEsBook(Long bookId) {
BookInfo bookInfo = bookInfoMapper.selectById(bookId);
IndexResponse response = esClient.index(i -> i
.index(EsConsts.BookIndex.INDEX_NAME)
.id(bookInfo.getId().toString())
.document(EsBookDto.build(bookInfo))
);
log.info("Indexed with version " + response.version());
}
}

View File

@ -1,94 +0,0 @@
package io.github.xxyopen.novel.core.task;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch.core.BulkRequest;
import co.elastic.clients.elasticsearch.core.BulkResponse;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.core.constant.EsConsts;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.es.EsBookDto;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 小说数据同步到 elasticsearch 任务
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enabled", havingValue = "true")
@Component
@RequiredArgsConstructor
@Slf4j
public class BookToEsTask {
private final BookInfoMapper bookInfoMapper;
private final ElasticsearchClient elasticsearchClient;
/**
* 每月凌晨做一次全量数据同步
*/
@SneakyThrows
@XxlJob("saveToEsJobHandler")
public ReturnT<String> saveToEs() {
try {
QueryWrapper<BookInfo> queryWrapper = new QueryWrapper<>();
List<BookInfo> bookInfos;
long maxId = 0;
for (; ; ) {
queryWrapper.clear();
queryWrapper
.orderByAsc(DatabaseConsts.CommonColumnEnum.ID.getName())
.gt(DatabaseConsts.CommonColumnEnum.ID.getName(), maxId)
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.last(DatabaseConsts.SqlEnum.LIMIT_30.getSql());
bookInfos = bookInfoMapper.selectList(queryWrapper);
if (bookInfos.isEmpty()) {
break;
}
BulkRequest.Builder br = new BulkRequest.Builder();
for (BookInfo book : bookInfos) {
br.operations(op -> op
.index(idx -> idx
.index(EsConsts.BookIndex.INDEX_NAME)
.id(book.getId().toString())
.document(EsBookDto.build(book))
)
).timeout(Time.of(t -> t.time("10s")));
maxId = book.getId();
}
BulkResponse result = elasticsearchClient.bulk(br.build());
// Log errors, if any
if (result.errors()) {
log.error("Bulk had errors");
for (BulkResponseItem item : result.items()) {
if (item.error() != null) {
log.error(item.error().reason());
}
}
}
}
return ReturnT.SUCCESS;
} catch (Exception e) {
log.error(e.getMessage(), e);
return ReturnT.FAIL;
}
}
}

View File

@ -5,13 +5,14 @@ import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/** /**
* JWT 工具类 * JWT 工具类
* *
@ -36,7 +37,6 @@ public class JwtUtils {
/** /**
* 根据用户ID生成JWT * 根据用户ID生成JWT
*
* @param uid 用户ID * @param uid 用户ID
* @param systemKey 系统标识 * @param systemKey 系统标识
* @return JWT * @return JWT
@ -51,7 +51,6 @@ public class JwtUtils {
/** /**
* 解析JWT返回用户ID * 解析JWT返回用户ID
*
* @param token JWT * @param token JWT
* @param systemKey 系统标识 * @param systemKey 系统标识
* @return 用户ID * @return 用户ID

View File

@ -2,6 +2,7 @@ package io.github.xxyopen.novel.core.wrapper;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletRequestWrapper;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -13,7 +14,7 @@ import java.util.Map;
*/ */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Map<String, String> REPLACE_RULE = new HashMap<>(); private static final Map<String,String> REPLACE_RULE = new HashMap<>();
static { static {
REPLACE_RULE.put("<", "&lt;"); REPLACE_RULE.put("<", "&lt;");
@ -33,8 +34,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
escapeValues[i] = values[i]; escapeValues[i] = values[i];
int index = i; int index = i;
REPLACE_RULE.forEach( REPLACE_RULE.forEach((k, v)-> escapeValues[index] = escapeValues[index].replaceAll(k, v));
(k, v) -> escapeValues[index] = escapeValues[index].replaceAll(k, v));
} }
return escapeValues; return escapeValues;
} }

View File

@ -21,8 +21,6 @@ public class AuthorInfoDto implements Serializable {
private Long id; private Long id;
private String penName;
private Integer status; private Integer status;
} }

View File

@ -1,130 +0,0 @@
package io.github.xxyopen.novel.dto.es;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.ZoneOffset;
/**
* Elasticsearch 存储小说 DTO
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EsBookDto {
/**
* id
*/
private Long id;
/**
* 作品方向;0-男频 1-女频
*/
private Integer workDirection;
/**
* 类别ID
*/
private Long categoryId;
/**
* 类别名
*/
private String categoryName;
/**
* 小说名
*/
private String bookName;
/**
* 作家id
*/
private Long authorId;
/**
* 作家名
*/
private String authorName;
/**
* 书籍描述
*/
private String bookDesc;
/**
* 评分;总分:10 真实评分 = score/10
*/
private Integer score;
/**
* 书籍状态;0-连载中 1-已完结
*/
private Integer bookStatus;
/**
* 点击量
*/
private Long visitCount;
/**
* 总字数
*/
private Integer wordCount;
/**
* 评论数
*/
private Integer commentCount;
/**
* 最新章节ID
*/
private Long lastChapterId;
/**
* 最新章节名
*/
private String lastChapterName;
/**
* 最新章节更新时间
*/
private Long lastChapterUpdateTime;
/**
* 是否收费;1-收费 0-免费
*/
private Integer isVip;
public static EsBookDto build(BookInfo bookInfo){
return EsBookDto.builder()
.id(bookInfo.getId())
.categoryId(bookInfo.getCategoryId())
.categoryName(bookInfo.getCategoryName())
.bookDesc(bookInfo.getBookDesc())
.bookName(bookInfo.getBookName())
.authorId(bookInfo.getAuthorId())
.authorName(bookInfo.getAuthorName())
.bookStatus(bookInfo.getBookStatus())
.commentCount(bookInfo.getCommentCount())
.isVip(bookInfo.getIsVip())
.score(bookInfo.getScore())
.visitCount(bookInfo.getVisitCount())
.wordCount(bookInfo.getWordCount())
.workDirection(bookInfo.getWorkDirection())
.lastChapterId(bookInfo.getLastChapterId())
.lastChapterName(bookInfo.getLastChapterName())
.lastChapterUpdateTime(bookInfo.getLastChapterUpdateTime()
.toInstant(ZoneOffset.ofHours(8)).toEpochMilli())
.build();
}
}

View File

@ -1,58 +0,0 @@
package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
/**
* 作家注册 请求DTO
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Data
public class AuthorRegisterReqDto {
@Schema(hidden = true)
private Long userId;
/**
* 笔名
*/
@Schema(description = "笔名", required = true)
@NotBlank(message = "笔名不能为空!")
private String penName;
/**
* 手机号码
*/
@Schema(description = "手机号码", required = true)
@NotBlank(message = "手机号不能为空!")
@Pattern(regexp = "^1[3|4|5|6|7|8|9][0-9]{9}$", message = "手机号格式不正确!")
private String telPhone;
/**
* QQ或微信账号
*/
@Schema(description = "QQ或微信账号", required = true)
@NotBlank(message = "QQ或微信账号不能为空")
private String chatAccount;
/**
* 电子邮箱
*/
@Schema(description = "电子邮箱", required = true)
@NotBlank(message = "电子邮箱不能为空!")
@Email(message = "邮箱格式不正确!")
private String email;
/**
* 作品方向;0-男频 1-女频
*/
@Schema(description = "作品方向;0-男频 1-女频", required = true)
@NotNull(message = "作品方向不能为空!")
@Min(0)
@Max(1)
private Integer workDirection;
}

View File

@ -1,65 +0,0 @@
package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 小说发布 请求DTO
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Data
public class BookAddReqDto {
/**
* 作品方向;0-男频 1-女频
*/
@Schema(description = "作品方向;0-男频 1-女频", required = true)
@NotNull
private Integer workDirection;
/**
* 类别ID
*/
@Schema(description = "类别ID", required = true)
@NotNull
private Long categoryId;
/**
* 类别名
*/
@Schema(description = "类别名", required = true)
@NotBlank
private String categoryName;
/**
* 小说封面地址
*/
@Schema(description = "小说封面地址", required = true)
@NotBlank
private String picUrl;
/**
* 小说名
*/
@Schema(description = "小说名", required = true)
@NotBlank
private String bookName;
/**
* 书籍描述
*/
@Schema(description = "书籍描述", required = true)
@NotBlank
private String bookDesc;
/**
* 是否收费;1-收费 0-免费
*/
@Schema(description = "是否收费;1-收费 0-免费", required = true)
@NotNull
private Integer isVip;
}

View File

@ -2,7 +2,6 @@ package io.github.xxyopen.novel.dto.req;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.github.xxyopen.novel.core.common.req.PageReqDto; import io.github.xxyopen.novel.core.common.req.PageReqDto;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.Data; import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
@ -20,43 +19,36 @@ public class BookSearchReqDto extends PageReqDto {
/** /**
* 搜索关键字 * 搜索关键字
*/ */
@Parameter(description = "搜索关键字")
private String keyword; private String keyword;
/** /**
* 作品方向 * 作品方向
*/ */
@Parameter(description = "作品方向") private Byte workDirection;
private Integer workDirection;
/** /**
* 分类ID * 分类ID
*/ */
@Parameter(description = "分类ID")
private Integer categoryId; private Integer categoryId;
/** /**
* 是否收费1收费0免费 * 是否收费1收费0免费
*/ */
@Parameter(description = "是否收费1收费0免费") private Byte isVip;
private Integer isVip;
/** /**
* 小说更新状态0连载中1已完结 * 小说更新状态0连载中1已完结
*/ */
@Parameter(description = "小说更新状态0连载中1已完结") private Byte bookStatus;
private Integer bookStatus;
/** /**
* 字数最小值 * 字数最小值
*/ */
@Parameter(description = "字数最小值")
private Integer wordCountMin; private Integer wordCountMin;
/** /**
* 字数最大值 * 字数最大值
*/ */
@Parameter(description = "字数最大值")
private Integer wordCountMax; private Integer wordCountMax;
/** /**
@ -65,7 +57,6 @@ public class BookSearchReqDto extends PageReqDto {
* 如果使用Post请求@RequestBody接收请求体参数默认解析日期格式为yyyy-MM-dd HH:mm:ss , * 如果使用Post请求@RequestBody接收请求体参数默认解析日期格式为yyyy-MM-dd HH:mm:ss ,
* 如果需要接收其他格式的参数则可以使用@JsonFormat注解 * 如果需要接收其他格式的参数则可以使用@JsonFormat注解
* */ * */
@Parameter(description = "最小更新时间")
@DateTimeFormat(pattern = "yyyy-MM-dd") @DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
private Date updateTimeMin; private Date updateTimeMin;
@ -73,6 +64,5 @@ public class BookSearchReqDto extends PageReqDto {
/** /**
* 排序字段 * 排序字段
*/ */
@Parameter(description = "排序字段") private String sort = "last_chapter_update_time desc";
private String sort;
} }

View File

@ -1,46 +0,0 @@
package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
/**
* 章节发布 请求DTO
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Data
public class ChapterAddReqDto {
/**
* 小说ID
*/
@Schema(description = "小说ID", required = true)
private Long bookId;
/**
* 章节名
*/
@NotBlank
@Schema(description = "章节名", required = true)
private String chapterName;
/**
* 章节内容
*/
@Schema(description = "章节内容", required = true)
@NotBlank
@Length(min = 50)
private String chapterContent;
/**
* 是否收费;1-收费 0-免费
*/
@Schema(description = "是否收费;1-收费 0-免费", required = true)
@NotNull
private Integer isVip;
}

View File

@ -1,40 +0,0 @@
package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
/**
* 章节发布 请求DTO
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Data
public class ChapterUpdateReqDto {
/**
* 章节名
*/
@NotBlank
@Schema(description = "章节名", required = true)
private String chapterName;
/**
* 章节内容
*/
@Schema(description = "章节内容", required = true)
@NotBlank
@Length(min = 50)
private String chapterContent;
/**
* 是否收费;1-收费 0-免费
*/
@Schema(description = "是否收费;1-收费 0-免费", required = true)
@NotNull
private Integer isVip;
}

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.req; package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
@ -16,11 +15,9 @@ public class UserCommentReqDto {
private Long userId; private Long userId;
@Schema(description = "小说ID", required = true)
@NotNull(message="小说ID不能为空") @NotNull(message="小说ID不能为空")
private Long bookId; private Long bookId;
@Schema(description = "评论内容", required = true)
@NotBlank(message="评论不能为空!") @NotBlank(message="评论不能为空!")
@Length(min = 10,max = 512) @Length(min = 10,max = 512)
private String commentContent; private String commentContent;

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.req; package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
@ -17,15 +16,12 @@ public class UserInfoUptReqDto {
private Long userId; private Long userId;
@Schema(description = "昵称")
@Length(min = 2,max = 10) @Length(min = 2,max = 10)
private String nickName; private String nickName;
@Schema(description = "头像地址") @Pattern(regexp="^/[^\s]{10,}\\.(png|jpg|jpeg|gif|bpm)$")
@Pattern(regexp="^/[^\s]{10,}\\.(png|PNG|jpg|JPG|jpeg|JPEG|gif|GIF|bpm|BPM)$")
private String userPhoto; private String userPhoto;
@Schema(description = "性别")
@Min(value = 0) @Min(value = 0)
@Max(value = 1) @Max(value = 1)
private Integer userSex; private Integer userSex;

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.req; package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import lombok.Data; import lombok.Data;
@ -14,13 +13,11 @@ import lombok.Data;
@Data @Data
public class UserLoginReqDto { public class UserLoginReqDto {
@Schema(description = "手机号", required = true, example = "18888888888") @NotBlank(message="手机号不能为空!")
@NotBlank(message = "手机号不能为空!") @Pattern(regexp="^1[3|4|5|6|7|8|9][0-9]{9}$",message="手机号格式不正确!")
@Pattern(regexp = "^1[3|4|5|6|7|8|9][0-9]{9}$", message = "手机号格式不正确!")
private String username; private String username;
@Schema(description = "密码", required = true, example = "123456") @NotBlank(message="密码不能为空!")
@NotBlank(message = "密码不能为空!")
private String password; private String password;
} }

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.req; package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import lombok.Data; import lombok.Data;
@ -15,16 +14,13 @@ import org.hibernate.validator.constraints.Length;
@Data @Data
public class UserRegisterReqDto { public class UserRegisterReqDto {
@Schema(description = "手机号", required = true)
@NotBlank(message="手机号不能为空!") @NotBlank(message="手机号不能为空!")
@Pattern(regexp="^1[3|4|5|6|7|8|9][0-9]{9}$",message="手机号格式不正确!") @Pattern(regexp="^1[3|4|5|6|7|8|9][0-9]{9}$",message="手机号格式不正确!")
private String username; private String username;
@Schema(description = "密码", required = true)
@NotBlank(message="密码不能为空!") @NotBlank(message="密码不能为空!")
private String password; private String password;
@Schema(description = "验证码", required = true)
@NotBlank(message="验证码不能为空!") @NotBlank(message="验证码不能为空!")
@Pattern(regexp="^\\d{4}$",message="验证码格式不正确!") @Pattern(regexp="^\\d{4}$",message="验证码格式不正确!")
private String velCode; private String velCode;
@ -32,7 +28,6 @@ public class UserRegisterReqDto {
/** /**
* 请求会话标识用来标识图形验证码属于哪个会话 * 请求会话标识用来标识图形验证码属于哪个会话
* */ * */
@Schema(description = "sessionId", required = true)
@NotBlank @NotBlank
@Length(min = 32,max = 32) @Length(min = 32,max = 32)
private String sessionId; private String sessionId;

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -17,13 +16,11 @@ public class BookCategoryRespDto {
/** /**
* 类别ID * 类别ID
*/ */
@Schema(description = "类别ID")
private Long id; private Long id;
/** /**
* 类别名 * 类别名
*/ */
@Schema(description = "类别名")
private String name; private String name;
} }

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -19,13 +18,11 @@ public class BookChapterAboutRespDto {
/** /**
* 章节总数 * 章节总数
*/ */
@Schema(description = "章节总数")
private Long chapterTotal; private Long chapterTotal;
/** /**
* 内容概要30字 * 内容概要30字
*/ */
@Schema(description = " 内容概要30字")
private String contentSummary; private String contentSummary;
} }

View File

@ -1,7 +1,6 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -24,45 +23,33 @@ public class BookChapterRespDto implements Serializable {
/** /**
* 章节ID * 章节ID
*/ * */
@Schema(description = "章节ID")
private Long id; private Long id;
/** /**
* 小说ID * 小说ID
*/ */
@Schema(description = "小说ID")
private Long bookId; private Long bookId;
/** /**
* 章节号 * 章节号
*/ */
@Schema(description = "章节号")
private Integer chapterNum; private Integer chapterNum;
/** /**
* 章节名 * 章节名
*/ */
@Schema(description = "章节名")
private String chapterName; private String chapterName;
/** /**
* 章节字数 * 章节字数
*/ */
@Schema(description = "章节字数")
private Integer chapterWordCount; private Integer chapterWordCount;
/** /**
* 章节更新时间 * 章节更新时间
*/ */
@Schema(description = "章节更新时间") @JsonFormat(pattern = "yyyy/MM/dd HH:dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime chapterUpdateTime; private LocalDateTime chapterUpdateTime;
/**
* 是否收费;1-收费 0-免费
*/
@Schema(description = "是否收费;1-收费 0-免费")
private Integer isVip;
} }

View File

@ -3,7 +3,6 @@ package io.github.xxyopen.novel.dto.resp;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.github.xxyopen.novel.core.json.serializer.UsernameSerializer; import io.github.xxyopen.novel.core.json.serializer.UsernameSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -19,36 +18,29 @@ import java.util.List;
@Builder @Builder
public class BookCommentRespDto { public class BookCommentRespDto {
@Schema(description = "评论总数")
private Long commentTotal; private Long commentTotal;
@Schema(description = "评论列表")
private List<CommentInfo> comments; private List<CommentInfo> comments;
@Data @Data
@Builder @Builder
public static class CommentInfo { public static class CommentInfo {
@Schema(description = "评论ID")
private Long id; private Long id;
@Schema(description = "评论内容")
private String commentContent; private String commentContent;
@Schema(description = "评论用户")
@JsonSerialize(using = UsernameSerializer.class) @JsonSerialize(using = UsernameSerializer.class)
private String commentUser; private String commentUser;
@Schema(description = "评论用户ID")
private Long commentUserId; private Long commentUserId;
@Schema(description = "评论用户头像")
private String commentUserPhoto;
@Schema(description = "评论时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime commentTime; private LocalDateTime commentTime;
} }
} }

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -17,19 +16,16 @@ public class BookContentAboutRespDto {
/** /**
* 小说信息 * 小说信息
*/ */
@Schema(description = "小说信息")
private BookInfoRespDto bookInfo; private BookInfoRespDto bookInfo;
/** /**
* 章节信息 * 章节信息
*/ */
@Schema(description = "章节信息")
private BookChapterRespDto chapterInfo; private BookChapterRespDto chapterInfo;
/** /**
* 章节内容 * 章节内容
*/ */
@Schema(description = "章节内容")
private String bookContent; private String bookContent;
} }

View File

@ -1,10 +1,7 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Builder;
import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data;
import lombok.*;
import java.time.LocalDateTime;
/** /**
* 小说信息 响应DTO * 小说信息 响应DTO
@ -13,107 +10,83 @@ import java.time.LocalDateTime;
* @date 2022/5/15 * @date 2022/5/15
*/ */
@Data @Data
@NoArgsConstructor
@AllArgsConstructor
@Builder @Builder
public class BookInfoRespDto { public class BookInfoRespDto {
/** /**
* ID * ID
*/ */
@Schema(description = "小说ID")
private Long id; private Long id;
/** /**
* 类别ID * 类别ID
*/ */
@Schema(description = "类别ID")
private Long categoryId; private Long categoryId;
/** /**
* 类别名 * 类别名
*/ */
@Schema(description = "类别名")
private String categoryName; private String categoryName;
/** /**
* 小说封面地址 * 小说封面地址
*/ */
@Schema(description = "小说封面地址")
private String picUrl; private String picUrl;
/** /**
* 小说名 * 小说名
*/ */
@Schema(description = "小说名")
private String bookName; private String bookName;
/** /**
* 作家id * 作家id
*/ */
@Schema(description = "作家id")
private Long authorId; private Long authorId;
/** /**
* 作家名 * 作家名
*/ */
@Schema(description = "作家名")
private String authorName; private String authorName;
/** /**
* 书籍描述 * 书籍描述
*/ */
@Schema(description = "书籍描述")
private String bookDesc; private String bookDesc;
/** /**
* 书籍状态;0-连载中 1-已完结 * 书籍状态;0-连载中 1-已完结
*/ */
@Schema(description = "书籍状态;0-连载中 1-已完结")
private Integer bookStatus; private Integer bookStatus;
/** /**
* 点击量 * 点击量
*/ */
@Schema(description = "点击量")
private Long visitCount; private Long visitCount;
/** /**
* 总字数 * 总字数
*/ */
@Schema(description = "总字数")
private Integer wordCount; private Integer wordCount;
/** /**
* 评论数 * 评论数
*/ */
@Schema(description = "评论数")
private Integer commentCount; private Integer commentCount;
/** /**
* 首章节ID * 首章节ID
*/ */
@Schema(description = "首章节ID")
private Long firstChapterId; private Long firstChapterId;
/** /**
* 最新章节ID * 最新章节ID
*/ */
@Schema(description = "最新章节ID")
private Long lastChapterId; private Long lastChapterId;
/** /**
* 最新章节名 * 最新章节名
*/ */
@Schema(description = "最新章节名")
private String lastChapterName; private String lastChapterName;
/**
* 最新章节更新时间
*/
@Schema(description = "最新章节更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime updateTime;
} }

View File

@ -1,7 +1,6 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.io.Serial; import java.io.Serial;
@ -23,61 +22,51 @@ public class BookRankRespDto implements Serializable {
/** /**
* ID * ID
*/ */
@Schema(description = "小说ID")
private Long id; private Long id;
/** /**
* 类别ID * 类别ID
*/ */
@Schema(description = "类别ID")
private Long categoryId; private Long categoryId;
/** /**
* 类别名 * 类别名
*/ */
@Schema(description = "类别名")
private String categoryName; private String categoryName;
/** /**
* 小说封面地址 * 小说封面地址
*/ */
@Schema(description = "小说封面地址")
private String picUrl; private String picUrl;
/** /**
* 小说名 * 小说名
*/ */
@Schema(description = "小说名")
private String bookName; private String bookName;
/** /**
* 作家名 * 作家名
*/ */
@Schema(description = "作家名")
private String authorName; private String authorName;
/** /**
* 书籍描述 * 书籍描述
*/ */
@Schema(description = "书籍描述")
private String bookDesc; private String bookDesc;
/** /**
* 总字数 * 总字数
*/ */
@Schema(description = "总字数")
private Integer wordCount; private Integer wordCount;
/** /**
* 最新章节名 * 最新章节名
*/ */
@Schema(description = "最新章节名")
private String lastChapterName; private String lastChapterName;
/** /**
* 最新章节更新时间 * 最新章节更新时间
*/ */
@Schema(description = "最新章节更新时间")
@JsonFormat(pattern = "MM/dd HH:mm") @JsonFormat(pattern = "MM/dd HH:mm")
private LocalDateTime lastChapterUpdateTime; private LocalDateTime lastChapterUpdateTime;

View File

@ -1,35 +0,0 @@
package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 小说内容 响应DTO
*
* @author xiongxiaoyang
* @date 2022/5/15
*/
@Data
@Builder
public class ChapterContentRespDto {
/**
* 章节标题
*/
@Schema(description = "章节名")
private String chapterName;
/**
* 章节内容
*/
@Schema(description = "章节内容")
private String chapterContent;
/**
* 是否收费;1-收费 0-免费
*/
@Schema(description = "是否收费;1-收费 0-免费")
private Integer isVip;
}

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
/** /**
@ -15,37 +14,31 @@ public class HomeBookRespDto {
/** /**
* 类型;0-轮播图 1-顶部栏 2-本周强推 3-热门推荐 4-精品推荐 * 类型;0-轮播图 1-顶部栏 2-本周强推 3-热门推荐 4-精品推荐
*/ */
@Schema(description = "类型;0-轮播图 1-顶部栏 2-本周强推 3-热门推荐 4-精品推荐")
private Integer type; private Integer type;
/** /**
* 推荐小说ID * 推荐小说ID
*/ */
@Schema(description = "小说ID")
private Long bookId; private Long bookId;
/** /**
* 小说封面地址 * 小说封面地址
*/ */
@Schema(description = "小说封面地址")
private String picUrl; private String picUrl;
/** /**
* 小说名 * 小说名
*/ */
@Schema(description = "小说名")
private String bookName; private String bookName;
/** /**
* 作家名 * 作家名
*/ */
@Schema(description = "作家名")
private String authorName; private String authorName;
/** /**
* 书籍描述 * 书籍描述
*/ */
@Schema(description = "书籍描述")
private String bookDesc; private String bookDesc;
} }

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.io.Serial; import java.io.Serial;
@ -21,12 +20,10 @@ public class HomeFriendLinkRespDto implements Serializable {
/** /**
* 链接名 * 链接名
*/ */
@Schema(description = "链接名")
private String linkName; private String linkName;
/** /**
* 链接url * 链接url
*/ */
@Schema(description = "链接url")
private String linkUrl; private String linkUrl;
} }

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -16,13 +15,11 @@ public class ImgVerifyCodeRespDto {
/** /**
* 当前会话ID用于标识改图形验证码属于哪个会话 * 当前会话ID用于标识改图形验证码属于哪个会话
* */ * */
@Schema(description = "sessionId")
private String sessionId; private String sessionId;
/** /**
* Base64 编码的验证码图片 * Base64 编码的验证码图片
* */ * */
@Schema(description = "Base64 编码的验证码图片")
private String img; private String img;
} }

View File

@ -1,7 +1,6 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -20,44 +19,37 @@ public class NewsInfoRespDto {
/** /**
* ID * ID
*/ */
@Schema(description = "新闻ID")
private Long id; private Long id;
/** /**
* 类别ID * 类别ID
*/ */
@Schema(description = "类别ID")
private Long categoryId; private Long categoryId;
/** /**
* 类别名 * 类别名
*/ */
@Schema(description = "类别名")
private String categoryName; private String categoryName;
/** /**
* 新闻来源 * 新闻来源
*/ */
@Schema(description = "新闻来源")
private String sourceName; private String sourceName;
/** /**
* 新闻标题 * 新闻标题
*/ */
@Schema(description = "新闻标题")
private String title; private String title;
/** /**
* 更新时间 * 更新时间
*/ */
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime updateTime; private LocalDateTime updateTime;
/** /**
* 新闻内容 * 新闻内容
* */ * */
@Schema(description = "新闻内容")
private String content; private String content;

View File

@ -1,33 +0,0 @@
package io.github.xxyopen.novel.dto.resp;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户评论响应 Dto
*
* @author xiongxiaoyang
* @date 2023/4/25
*/
@Data
@Builder
public class UserCommentRespDto {
@Schema(description = "评论内容")
private String commentContent;
@Schema(description = "评论小说封面")
private String commentBookPic;
@Schema(description = "评论小说")
private String commentBook;
@Schema(description = "评论时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime commentTime;
}

View File

@ -1,34 +0,0 @@
package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 用户信息 响应DTO
*
* @author xiongxiaoyang
* @date 2022/5/22
*/
@Data
@Builder
public class UserInfoRespDto {
/**
* 昵称
* */
@Schema(description = "昵称")
private String nickName;
/**
* 用户头像
* */
@Schema(description = "用户头像")
private String userPhoto;
/**
* 用户性别
* */
@Schema(description = "用户性别")
private Integer userSex;
}

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -13,12 +12,9 @@ import lombok.Data;
@Builder @Builder
public class UserLoginRespDto { public class UserLoginRespDto {
@Schema(description = "用户ID")
private Long uid; private Long uid;
@Schema(description = "用户昵称")
private String nickName; private String nickName;
@Schema(description = "用户token")
private String token; private String token;
} }

View File

@ -1,6 +1,5 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -13,9 +12,7 @@ import lombok.Data;
@Builder @Builder
public class UserRegisterRespDto { public class UserRegisterRespDto {
@Schema(description = "用户ID")
private Long uid; private Long uid;
@Schema(description = "用户token")
private String token; private String token;
} }

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager.cache; package io.github.xxyopen.novel.manager;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts; import io.github.xxyopen.novel.core.constant.CacheConsts;
@ -6,12 +6,12 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.AuthorInfo; import io.github.xxyopen.novel.dao.entity.AuthorInfo;
import io.github.xxyopen.novel.dao.mapper.AuthorInfoMapper; import io.github.xxyopen.novel.dao.mapper.AuthorInfoMapper;
import io.github.xxyopen.novel.dto.AuthorInfoDto; import io.github.xxyopen.novel.dto.AuthorInfoDto;
import java.util.Objects;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Objects;
/** /**
* 作家信息 缓存管理类 * 作家信息 缓存管理类
* *
@ -27,8 +27,8 @@ public class AuthorInfoCacheManager {
/** /**
* 查询作家信息并放入缓存中 * 查询作家信息并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER
value = CacheConsts.AUTHOR_INFO_CACHE_NAME, unless = "#result == null") , value = CacheConsts.AUTHOR_INFO_CACHE_NAME)
public AuthorInfoDto getAuthor(Long userId) { public AuthorInfoDto getAuthor(Long userId) {
QueryWrapper<AuthorInfo> queryWrapper = new QueryWrapper<>(); QueryWrapper<AuthorInfo> queryWrapper = new QueryWrapper<>();
queryWrapper queryWrapper
@ -40,14 +40,8 @@ public class AuthorInfoCacheManager {
} }
return AuthorInfoDto.builder() return AuthorInfoDto.builder()
.id(authorInfo.getId()) .id(authorInfo.getId())
.penName(authorInfo.getPenName())
.status(authorInfo.getStatus()).build(); .status(authorInfo.getStatus()).build();
} }
@CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.AUTHOR_INFO_CACHE_NAME)
public void evictAuthorCache() {
// 调用此方法自动清除作家信息的缓存
}
} }

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager.cache; package io.github.xxyopen.novel.manager;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts; import io.github.xxyopen.novel.core.constant.CacheConsts;
@ -6,11 +6,12 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.BookCategory; import io.github.xxyopen.novel.dao.entity.BookCategory;
import io.github.xxyopen.novel.dao.mapper.BookCategoryMapper; import io.github.xxyopen.novel.dao.mapper.BookCategoryMapper;
import io.github.xxyopen.novel.dto.resp.BookCategoryRespDto; import io.github.xxyopen.novel.dto.resp.BookCategoryRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 小说分类 缓存管理类 * 小说分类 缓存管理类
* *
@ -26,8 +27,8 @@ public class BookCategoryCacheManager {
/** /**
* 根据作品方向查询小说分类列表并放入缓存中 * 根据作品方向查询小说分类列表并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER
value = CacheConsts.BOOK_CATEGORY_LIST_CACHE_NAME) , value = CacheConsts.BOOK_CATEGORY_LIST_CACHE_NAME)
public List<BookCategoryRespDto> listCategory(Integer workDirection) { public List<BookCategoryRespDto> listCategory(Integer workDirection) {
QueryWrapper<BookCategory> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookCategory> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookCategoryTable.COLUMN_WORK_DIRECTION, workDirection); queryWrapper.eq(DatabaseConsts.BookCategoryTable.COLUMN_WORK_DIRECTION, workDirection);

View File

@ -1,11 +1,10 @@
package io.github.xxyopen.novel.manager.cache; package io.github.xxyopen.novel.manager;
import io.github.xxyopen.novel.core.constant.CacheConsts; import io.github.xxyopen.novel.core.constant.CacheConsts;
import io.github.xxyopen.novel.dao.entity.BookChapter; import io.github.xxyopen.novel.dao.entity.BookChapter;
import io.github.xxyopen.novel.dao.mapper.BookChapterMapper; import io.github.xxyopen.novel.dao.mapper.BookChapterMapper;
import io.github.xxyopen.novel.dto.resp.BookChapterRespDto; import io.github.xxyopen.novel.dto.resp.BookChapterRespDto;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -24,8 +23,8 @@ public class BookChapterCacheManager {
/** /**
* 查询小说章节信息并放入缓存中 * 查询小说章节信息并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER
value = CacheConsts.BOOK_CHAPTER_CACHE_NAME) , value = CacheConsts.BOOK_CHAPTER_CACHE_NAME)
public BookChapterRespDto getChapter(Long chapterId) { public BookChapterRespDto getChapter(Long chapterId) {
BookChapter bookChapter = bookChapterMapper.selectById(chapterId); BookChapter bookChapter = bookChapterMapper.selectById(chapterId);
return BookChapterRespDto.builder() return BookChapterRespDto.builder()
@ -35,14 +34,8 @@ public class BookChapterCacheManager {
.chapterName(bookChapter.getChapterName()) .chapterName(bookChapter.getChapterName())
.chapterWordCount(bookChapter.getWordCount()) .chapterWordCount(bookChapter.getWordCount())
.chapterUpdateTime(bookChapter.getUpdateTime()) .chapterUpdateTime(bookChapter.getUpdateTime())
.isVip(bookChapter.getIsVip())
.build(); .build();
} }
@CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_CHAPTER_CACHE_NAME)
public void evictBookChapterCache(Long chapterId) {
// 调用此方法自动清除小说章节信息的缓存
}
} }

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager.cache; package io.github.xxyopen.novel.manager;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts; import io.github.xxyopen.novel.core.constant.CacheConsts;
@ -6,7 +6,6 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.BookContent; import io.github.xxyopen.novel.dao.entity.BookContent;
import io.github.xxyopen.novel.dao.mapper.BookContentMapper; import io.github.xxyopen.novel.dao.mapper.BookContentMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -25,8 +24,8 @@ public class BookContentCacheManager {
/** /**
* 查询小说内容并放入缓存中 * 查询小说内容并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER
value = CacheConsts.BOOK_CONTENT_CACHE_NAME) , value = CacheConsts.BOOK_CONTENT_CACHE_NAME)
public String getBookContent(Long chapterId) { public String getBookContent(Long chapterId) {
QueryWrapper<BookContent> contentQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookContent> contentQueryWrapper = new QueryWrapper<>();
contentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId) contentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId)
@ -35,11 +34,5 @@ public class BookContentCacheManager {
return bookContent.getContent(); return bookContent.getContent();
} }
@CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.BOOK_CONTENT_CACHE_NAME)
public void evictBookContentCache(Long chapterId) {
// 调用此方法自动清除小说内容信息的缓存
}
} }

View File

@ -0,0 +1,78 @@
package io.github.xxyopen.novel.manager;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;
import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.BookChapter;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookChapterMapper;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 小说信息 缓存管理类
*
* @author xiongxiaoyang
* @date 2022/5/12
*/
@Component
@RequiredArgsConstructor
public class BookInfoCacheManager {
private final BookInfoMapper bookInfoMapper;
private final BookChapterMapper bookChapterMapper;
/**
* 查询小说信息并放入缓存中
*/
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER
, value = CacheConsts.BOOK_INFO_CACHE_NAME)
public BookInfoRespDto getBookInfo(Long id) {
// 查询基础信息
BookInfo bookInfo = bookInfoMapper.selectById(id);
// 查询首章ID
QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
queryWrapper
.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, id)
.orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
BookChapter firstBookChapter = bookChapterMapper.selectOne(queryWrapper);
// 组装响应对象
return BookInfoRespDto.builder()
.id(bookInfo.getId())
.bookName(bookInfo.getBookName())
.bookDesc(bookInfo.getBookDesc())
.bookStatus(bookInfo.getBookStatus())
.authorId(bookInfo.getAuthorId())
.authorName(bookInfo.getAuthorName())
.categoryId(bookInfo.getCategoryId())
.categoryName(bookInfo.getCategoryName())
.commentCount(bookInfo.getCommentCount())
.firstChapterId(firstBookChapter.getId())
.lastChapterId(bookInfo.getLastChapterId())
.picUrl(bookInfo.getPicUrl())
.visitCount(bookInfo.getVisitCount())
.wordCount(bookInfo.getWordCount())
.build();
}
/**
* 查询每个类别下最新更新的 500 个小说ID列表并放入缓存中 1 个小时
*/
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER
, value = CacheConsts.LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME)
public List<Long> getLastUpdateIdList(Long categoryId) {
QueryWrapper<BookInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookTable.COLUMN_CATEGORY_ID, categoryId)
.orderByDesc(DatabaseConsts.BookTable.COLUMN_LAST_CHAPTER_UPDATE_TIME)
.last(DatabaseConsts.SqlEnum.LIMIT_500.getSql());
return bookInfoMapper.selectList(queryWrapper).stream().map(BookInfo::getId).toList();
}
}

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager.cache; package io.github.xxyopen.novel.manager;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts; import io.github.xxyopen.novel.core.constant.CacheConsts;
@ -6,11 +6,12 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.BookInfo; import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper; import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.resp.BookRankRespDto; import io.github.xxyopen.novel.dto.resp.BookRankRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 小说排行榜 缓存管理类 * 小说排行榜 缓存管理类
* *
@ -26,44 +27,40 @@ public class BookRankCacheManager {
/** /**
* 查询小说点击榜列表并放入缓存中 * 查询小说点击榜列表并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER
value = CacheConsts.BOOK_VISIT_RANK_CACHE_NAME) , value = CacheConsts.BOOK_VISIT_RANK_CACHE_NAME)
public List<BookRankRespDto> listVisitRankBooks() { public List<BookRankRespDto> listVisitRankBooks() {
QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>();
bookInfoQueryWrapper.orderByDesc(DatabaseConsts.BookTable.COLUMN_VISIT_COUNT); bookInfoQueryWrapper.orderByDesc(DatabaseConsts.BookTable.COLUMN_VISIT_COUNT);
return listRankBooks(bookInfoQueryWrapper); return getBookRankRespDtos(bookInfoQueryWrapper);
} }
/** /**
* 查询小说新书榜列表并放入缓存中 * 查询小说新书榜列表并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER
value = CacheConsts.BOOK_NEWEST_RANK_CACHE_NAME) , value = CacheConsts.BOOK_NEWEST_RANK_CACHE_NAME)
public List<BookRankRespDto> listNewestRankBooks() { public List<BookRankRespDto> listNewestRankBooks() {
QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>();
bookInfoQueryWrapper bookInfoQueryWrapper
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()); .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName());
return listRankBooks(bookInfoQueryWrapper); return getBookRankRespDtos(bookInfoQueryWrapper);
} }
/** /**
* 查询小说更新榜列表并放入缓存中 * 查询小说更新榜列表并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER
value = CacheConsts.BOOK_UPDATE_RANK_CACHE_NAME) , value = CacheConsts.BOOK_UPDATE_RANK_CACHE_NAME)
public List<BookRankRespDto> listUpdateRankBooks() { public List<BookRankRespDto> listUpdateRankBooks() {
QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>();
bookInfoQueryWrapper bookInfoQueryWrapper
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.orderByDesc(DatabaseConsts.CommonColumnEnum.UPDATE_TIME.getName()); .orderByDesc(DatabaseConsts.CommonColumnEnum.UPDATE_TIME.getName());
return listRankBooks(bookInfoQueryWrapper); return getBookRankRespDtos(bookInfoQueryWrapper);
} }
private List<BookRankRespDto> listRankBooks(QueryWrapper<BookInfo> bookInfoQueryWrapper) { private List<BookRankRespDto> getBookRankRespDtos(QueryWrapper<BookInfo> bookInfoQueryWrapper) {
bookInfoQueryWrapper bookInfoQueryWrapper.last(DatabaseConsts.SqlEnum.LIMIT_30.getSql());
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.last(DatabaseConsts.SqlEnum.LIMIT_30.getSql());
return bookInfoMapper.selectList(bookInfoQueryWrapper).stream().map(v -> { return bookInfoMapper.selectList(bookInfoQueryWrapper).stream().map(v -> {
BookRankRespDto respDto = new BookRankRespDto(); BookRankRespDto respDto = new BookRankRespDto();
respDto.setId(v.getId()); respDto.setId(v.getId());

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager.cache; package io.github.xxyopen.novel.manager;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts; import io.github.xxyopen.novel.core.constant.CacheConsts;
@ -6,11 +6,12 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.HomeFriendLink; import io.github.xxyopen.novel.dao.entity.HomeFriendLink;
import io.github.xxyopen.novel.dao.mapper.HomeFriendLinkMapper; import io.github.xxyopen.novel.dao.mapper.HomeFriendLinkMapper;
import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto; import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 友情链接 缓存管理类 * 友情链接 缓存管理类
* *
@ -26,8 +27,8 @@ public class FriendLinkCacheManager {
/** /**
* 友情链接列表查询并放入缓存中 * 友情链接列表查询并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER
value = CacheConsts.HOME_FRIEND_LINK_CACHE_NAME) , value = CacheConsts.HOME_FRIEND_LINK_CACHE_NAME)
public List<HomeFriendLinkRespDto> listFriendLinks() { public List<HomeFriendLinkRespDto> listFriendLinks() {
// 从友情链接表中查询出友情链接列表 // 从友情链接表中查询出友情链接列表
QueryWrapper<HomeFriendLink> queryWrapper = new QueryWrapper<>(); QueryWrapper<HomeFriendLink> queryWrapper = new QueryWrapper<>();

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