diff --git a/.gitignore b/.gitignore index 4781089cc89ad4f5cd59530a98b9b134818d5fca..ef9d58d20a8824d2075f4a16938bc313f0c224d2 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ build/ .vscode/ /logs/ +/.shardingsphere/ diff --git a/README.md b/README.md index 82f2c6114e383a1c452726e4fc4859c42b58bf0e..8caf41c3c880d18fbfd43ea5980170549e4a52f4 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,29 @@ -[![index]( https://s1.ax1x.com/2022/05/17/O5tgbR.png )]( https://curl.qcloud.com/kgMaOjoq ) +[![index]( https://youdoc.github.io/img/tencent.jpg )]( https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=736e609d66e0ac4e57813316cec6fd0b&from=console ) + +

+ Java 17 + Spring Boot 3 + Vue 3
+ Github stars + Github forks + Gitee stars + Gitee forks + visitors +

-[![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 开发的前后端分离的**学习型**小说项目,配备详细的项目教程手把手教你**从零开始**开发上线一个生产级别的 Java 系统,由小说门户系统、作家后台管理系统、平台后台管理系统、爬虫管理系统等多个子系统构成。包括小说推荐、作品检索、小说排行榜、小说阅读、小说评论、充值订阅、新闻发布等功能。 +novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开发的前后端分离**学习型** +小说项目,配备[保姆级教程](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-cloud) | [码云](https://gitee.com/novel_dev_team/novel-cloud) -- 前端项目(更新中):[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) |[演示地址](http://47.106.243.172:8888/) +- 前端项目(更新中):[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) ## 开发环境 @@ -21,41 +31,51 @@ novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开 - Redis 7.0 - Elasticsearch 8.2.0(可选) - RabbitMQ 3.10.2(可选) +- XXL-JOB 2.3.1(可选) - JDK 17 - Maven 3.8 - IntelliJ IDEA 2021.3(可选) - Node 16.14 +**注:Elasticsearch、RabbitMQ 和 XXL-JOB 默认关闭,可通过 application.yml 配置文件中相应的`enable`配置属性开启。** + ## 后端技术选型 -| 技术 | 版本 | 说明 | 官网 | 学习 | -|:----------------|:--------------:|---------------------| --------------------------------------- | :-------------------------------------------------: | -| Spring Boot | 3.0.0-SNAPSHOT | 容器 + MVC 框架 | https://spring.io/projects/spring-boot | [进入](https://youdoc.github.io/course/novel/11.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/) | -| JJWT | 0.11.5 | JWT 登录支持 | https://github.com/jwtk/jjwt | - | -| 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) | -| 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) | -| Elasticsearch | 8.2.0 | 搜索引擎服务 | https://www.elastic.co | [进入](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) | -| RabbitMQ | 3.10.2 | 开源消息中间件 | https://www.rabbitmq.com | [进入](https://www.rabbitmq.com/tutorials/tutorial-one-java.html) | -| Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 | https://undertow.io | [进入](https://undertow.io/documentation.html) | -| Docker | - | 应用容器引擎 | https://www.docker.com/ | - | -| Jenkins | - | 自动化部署工具 | https://github.com/jenkinsci/jenkins | - | -| Sonarqube | - | 代码质量控制 | https://www.sonarqube.org/ | - | +| 技术 | 版本 | 说明 | 官网 | 学习 | +|---------------------|:--------------:|---------------------| --------------------------------------- |:-----------------------------------------------------------------------------------------------------------------------------:| +| Spring Boot | 3.0.0 | 容器 + MVC 框架 | [进入](https://spring.io/projects/spring-boot) | [进入](https://docs.spring.io/spring-boot/docs/3.0.0/reference/html) | +| MyBatis | 3.5.9 | ORM 框架 | [进入](http://www.mybatis.org) | [进入](https://mybatis.org/mybatis-3/zh/index.html) | +| MyBatis-Plus | 3.5.3 | MyBatis 增强工具 | [进入](https://baomidou.com/) | [进入](https://baomidou.com/pages/24112f/) | +| JJWT | 0.11.5 | JWT 登录支持 | [进入](https://github.com/jwtk/jjwt) | - | +| 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) | +| Redis | 7.0 | 分布式缓存支持 | [进入](https://redis.io) | [进入](https://redis.io/docs) | +| Redisson | 3.17.4 | 分布式锁实现 | [进入](https://github.com/redisson/redisson) | [进入](https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95) | +| MySQL | 8.0 | 数据库服务 | [进入](https://www.mysql.com) | [进入](https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html) | +| ShardingSphere-JDBC | 5.1.1 | 数据库分库分表支持 | [进入](https://shardingsphere.apache.org) | [进入](https://shardingsphere.apache.org/document/5.1.1/cn/overview) | +| Elasticsearch | 8.2.0 | 搜索引擎服务 | [进入](https://www.elastic.co) | [进入](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) | +| 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) | +| Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 | [进入](https://undertow.io) | [进入](https://undertow.io/documentation.html) | +| Docker | - | 应用容器引擎 | [进入](https://www.docker.com/) | - | +| Jenkins | - | 自动化部署工具 | [进入](https://github.com/jenkinsci/jenkins) | - | +| Sonarqube | - | 代码质量控制 | [进入](https://www.sonarqube.org/) | - | **注:更多热门新技术待集成。** + ## 前端技术选型 | 技术 | 版本 | 说明 | 官网 | 学习 | | :----------------- | :-----: | -------------------------- | --------------------------------------- | :-------------------------------------------------: | -| 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/zh/index.html | [进入](https://router.vuejs.org/zh/guide/) | -| 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/zh-CN/ | [进入](https://element-plus.org/zh-CN/guide/design.html) | +| Vue.js | 3.2.13 | 渐进式 JavaScript 框架 | [进入](https://vuejs.org) | [进入](https://staging-cn.vuejs.org/guide/introduction.html) | +| Vue Router | 4.0.15 | Vue.js 的官方路由 | [进入](https://router.vuejs.org) | [进入](https://router.vuejs.org/zh/guide/) | +| axios | 0.27.2 | 基于 promise 的网络请求库 | [进入](https://axios-http.com) | [进入](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) | -## 编码规范 +## 编码规范 - 规范方式:严格遵守阿里编码规约。 - 命名统一:简介最大程度上达到了见名知意。 @@ -82,6 +102,8 @@ io | | +- resp -- 接口响应工具及响应数据格式封装 | | +- util -- 通用工具 | | + | +- annotation -- 自定义注解类 + | +- aspect -- Spring AOP 切面 | +- auth -- 用户认证授权相关 | +- config -- 业务相关配置 | +- constant -- 业务相关常量 @@ -162,104 +184,52 @@ io ![img](https://oscimg.oschina.net/oscnet/up-f849960f4c1303fea77d26e64fc505a7180.png) +11. 接口文档 -## 安装步骤 - -此安装步骤的前提是需要保证上一节的开发环境可用。 - -- 下载后端源码 - -``` -git clone https://gitee.com/novel_dev_team/novel.git -``` - -- 数据库文件导入 - - 1. 新建数据库(建议 novel) - - 2. 解压后端源码`doc/sql/novel.sql.zip`压缩包,得到数据库结构文件`novel_struc.sql`和数据库小说数据文件`novel_data.sql` - - 3. 导入`novel_struct.sql`数据库结构文件 +![img](https://youdoc.github.io/img/novel/SwaggerUI.png) - 4. 导入`novel_data.sql`数据库小说数据文件 - -- novel 后端服务安装 - - 1. 修改`src/resources/application.yml`配置文件中的数据源配置 - - ``` - 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`连接配置 - - ``` - spring: - redis: - host: 127.0.0.1 - port: 6379 - password: 123456 - ``` - - 3. 项目根目录下运行如下命令来启动后端服务(有安装 IDE 的可以导入源码到 IDE 中运行) - - ``` - mvn spring-boot:run - ``` - - 4. 根据前后端的实际部署情况,修改`application.yml`中的跨域配置(默认情况可忽略此步骤) +## 安装步骤 -- 下载前端前台门户系统源码 +👉 [立即查看](https://docs.xxyopen.com/course/novel/#%E5%AE%89%E8%A3%85%E6%AD%A5%E9%AA%A4) -``` -git clone https://gitee.com/novel_dev_team/novel-front-web.git -``` +## 联系我们 -- novel-front-web 前端前台门户系统安装 +👉 [立即查看](https://novel.xxyopen.com/service.htm) - 1. 根据前后端的实际部署情况,修改`.env.development`中的`VUE_APP_BASE_API_URL`属性(默认情况可忽略此步骤) +## 问题 - 2. `yarn`安装 +### 为什么有 novel/novel-cloud 学习版? - ``` - npm install -g yarn - ``` +最开始是没有学习版的,只有一个爬虫/原创小说项目(最终发展成为 [novel-plus](https://github.com/201206030/novel-plus) +项目),用户群体大部分是对小说有兴趣,想自建一个干净无广告的小说网站的个人和站长。 - 3. 项目根目录下运行如下命令来安装项目依赖 +后面随着使用人数逐渐增加,想通过这个项目来学习 Java 技术的人数也多了起来,对这部分用户来说,之前的项目用来学习很困难,具体原因如下: - ``` - yarn install - ``` - 4. 项目根目录下运行如下命令启动 +1. novel-plus 功能模块比较多,重复性的增删改查占了大部分,而用户时间是有限的,很难在有限的时间内筛选出对自己有帮助的功能模块来学习。 +2. novel-plus 追求的是系统稳定,用户很难在其中学习到最新的技术。 +3. novel-plus 代码规范性不够,受限于开发时间限制,代码开发时没有选择一个标准化的规范去参考。 +4. novel-plus 文档缺失,由于功能比较多,整个系统的教程编写需要花费大量时间,即使教程最终上线成功,用户也不可能有那么多时间也没有意义去学习所有的功能。 - ``` - yarn serve - ``` - 5. 浏览器通过`http://localhost:1024`来访问 +最终,novel(单体架构) 和 novel-cloud(微服务架构)诞生了,这两个项目在保证核心流程完整的同时,从 novel-plus +中选用了一些有代表性的功能,使用最新技术栈(不间断地更新和集成新技术),在[保姆级教程](https://docs.xxyopen.com/course/novel)的帮助下,尽量保证每一个功能都能让你学到不重复的技术。 -## 项目教程 +所以这两个项目我的重点是去堆技术而不是去堆功能,功能只是其中的辅助,堆太多的重复性增删改查功能没有意义,对学习的帮助也不大。 -[手把手教你从零开始开发上线一个生产级别的小说系统](https://youdoc.github.io/course/novel/3.html) - -## 公众号 +### 谁适合使用 novel/novel-cloud 学习版项目? -关注公众号接收项目最新动态,获取`Spring Boot 3`学习笔记! +如果对下面任何一个问题你能回答 "是": -加微信群交流,公众号后台回复「**微信群**」即可。 +1. 你没有项目经验,想学习如何从零开始开发上线一个生产级别的 Java 项目? +2. 你有项目经验,但是公司技术栈太落后,想学习最新的主流开发技术? -![微信公众号](https://youdoc.github.io/img/qrcode_for_gh.jpg) +那么,本项目正是你需要的。 -## 赞赏支持 +### 谁暂时还不适合使用 novel/novel-cloud 学习版项目? -开源项目不易,若此项目能得到你的青睐,那么你可以赞赏支持作者持续开发与维护。 +如果对下面任何一个问题你能回答 "是": -- 更完善的文档教程 -- 服务器的费用也是一笔开销 -- 为用户提供更好的开发环境 -- 一杯咖啡 +1. 你不懂 Java ? +2. 你只是想搭建一个小说网站使用? +3. 你想找一个完整的 Java 商用项目,有时间也有耐心去学习项目中的方方面面? -![mini-code](https://s1.ax1x.com/2020/10/31/BUQJwq.png) +那么,太遗憾了,本项目暂时不适合你,请使用 [novel-plus](https://github.com/201206030/novel-plus)。 diff --git a/doc/sql/README.md b/doc/sql/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7d28d8364352d3d3ee61b138014f6f470cd863a2 --- /dev/null +++ b/doc/sql/README.md @@ -0,0 +1,4 @@ +1. 初始状态下,MySQL 只需要执行 `novel.sql` 文件即可正常运行本系统 +2. 只有开启 XXL-JOB 的功能,才需要执行 `xxl-job.sql` 文件 +3. 只有开启 ShardingSphere-JDBC 的功能,才需要执行 `shardingsphere-jdbc.sql` 文件 + diff --git a/doc/sql/shardingsphere-jdbc.sql b/doc/sql/shardingsphere-jdbc.sql new file mode 100644 index 0000000000000000000000000000000000000000..b2daf1936bafd7cdf04e5eb17a80b17af2ac55ac --- /dev/null +++ b/doc/sql/shardingsphere-jdbc.sql @@ -0,0 +1,171 @@ +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(); \ No newline at end of file diff --git a/doc/sql/xxl-job.sql b/doc/sql/xxl-job.sql new file mode 100644 index 0000000000000000000000000000000000000000..ceed3714d1483e449ff28daa63a8ed5d1310469a --- /dev/null +++ b/doc/sql/xxl-job.sql @@ -0,0 +1,190 @@ +# +# 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 + ); \ No newline at end of file diff --git a/doc/style/README.md b/doc/style/README.md new file mode 100644 index 0000000000000000000000000000000000000000..41b26358aa4db8023956270a5fa4506a06556248 --- /dev/null +++ b/doc/style/README.md @@ -0,0 +1,3 @@ +IntelliJ IDEA 中导入 `intellij-java-google-style.xml` 文件: + +`Preferences` => `Editor` => `Code Style` => `Java` => `Schema` => `Import Schema` \ No newline at end of file diff --git a/doc/style/intellij-java-google-style.xml b/doc/style/intellij-java-google-style.xml new file mode 100644 index 0000000000000000000000000000000000000000..fb6821ed742b159eb5676c828ddf6e8479186abe --- /dev/null +++ b/doc/style/intellij-java-google-style.xml @@ -0,0 +1,35 @@ + + + + + + + diff --git a/pom.xml b/pom.xml index 47b6466bdfc81bf6c2b78934db93203da905cfe0..75139fde0e634fb10fb8da919869f56f229eaf51 100644 --- a/pom.xml +++ b/pom.xml @@ -1,48 +1,42 @@ - 4.0.0 org.springframework.boot spring-boot-starter-parent - 3.0.0-SNAPSHOT + 3.3.0 io.github.xxyopen novel - 3.1.0 + 3.5.0-SNAPSHOT novel Spring Boot 3 + Vue 3 构建的前后端分离小说系统 - 17 - 3.5.1 - 6.0.0-SNAPSHOT + 21 + 3.5.6 + 3.5.1 0.11.5 - 8.2.0 + 2.3.1 + 1.8.4 + 5.2.1 + 3.19.1 + 3.0.0-M1 + 2.5.0 + org.springframework.boot spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - - - org.springframework.boot - spring-boot-starter-undertow com.baomidou - mybatis-plus-boot-starter + mybatis-plus-spring-boot3-starter ${mybatis-plus.version} @@ -50,7 +44,7 @@ com.baomidou mybatis-plus-generator - ${mybatis-plus.version} + ${mybatis-plus-generator.version} test @@ -99,26 +93,92 @@ hibernate-validator - - co.elastic.clients - elasticsearch-java - ${elasticsearch.version} + org.springframework.boot + spring-boot-starter-data-elasticsearch + + - com.fasterxml.jackson.core - jackson-databind + org.springframework.boot + spring-boot-starter-amqp + + + + + com.xuxueli + xxl-job-core + ${xxl-job.version} - + + + com.alibaba.csp + sentinel-core + ${sentinel.version} + + + com.alibaba.csp + sentinel-parameter-flow-control + ${sentinel.version} + + + + + org.apache.shardingsphere + shardingsphere-jdbc-core-spring-boot-starter + ${shardingsphere-jdbc.version} + + + com.h2database + h2 + runtime + + + org.springframework.boot - spring-boot-starter-amqp + spring-boot-starter-actuator + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + + org.springframework.boot + spring-boot-starter-security + + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + - mysql - mysql-connector-java + org.springframework.boot + spring-boot-starter-aop + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc-openapi.version} + + + + + org.springframework.boot + spring-boot-starter-mail + + + + com.mysql + mysql-connector-j runtime @@ -136,6 +196,14 @@ spring-boot-starter-test test + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-mysql + @@ -152,43 +220,40 @@ + + maven-compiler-plugin + + + ${java.version} + + ${java.version} + + - spring-milestones - Spring Milestones - https://repo.spring.io/milestone + aliyun + https://maven.aliyun.com/repository/public + + true + false - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - false - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone + aliyun + https://maven.aliyun.com/repository/public + + true + false - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - false - - diff --git a/src/main/java/io/github/xxyopen/novel/NovelApplication.java b/src/main/java/io/github/xxyopen/novel/NovelApplication.java index 5f5d08291d126d8e3810c77532f2f1a0d6eb6164..1f1c8196575dfecc0bb141fb60b3d5f380db7896 100644 --- a/src/main/java/io/github/xxyopen/novel/NovelApplication.java +++ b/src/main/java/io/github/xxyopen/novel/NovelApplication.java @@ -4,12 +4,15 @@ import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.ApplicationContext; 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 java.util.Map; @@ -20,21 +23,30 @@ import java.util.Map; @Slf4j public class NovelApplication { - public static void main(String[] args) { - SpringApplication.run(NovelApplication.class, args); - } - - @Bean - public CommandLineRunner commandLineRunner(ApplicationContext context){ - return args -> { - Map beans = context.getBeansOfType(CacheManager.class); - log.info("加载了如下缓存管理器:"); - beans.forEach((k,v)->{ - log.info("{}:{}",k,v.getClass().getName()); - log.info("缓存:{}",v.getCacheNames()); - }); - - }; - } + public static void main(String[] args) { + SpringApplication.run(NovelApplication.class, args); + } + + @Bean + public CommandLineRunner commandLineRunner(ApplicationContext context) { + return args -> { + Map beans = context.getBeansOfType(CacheManager.class); + log.info("加载了如下缓存管理器:"); + beans.forEach((k, v) -> { + log.info("{}:{}", k, v.getClass().getName()); + log.info("缓存:{}", v.getCacheNames()); + }); + + }; + } + + @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(); + } } diff --git a/src/main/java/io/github/xxyopen/novel/controller/author/AuthorController.java b/src/main/java/io/github/xxyopen/novel/controller/author/AuthorController.java index f31188108bad473939a8453eb63098dc66986a32..bd5658279527bec0411cefc3031cae73d29574e7 100644 --- a/src/main/java/io/github/xxyopen/novel/controller/author/AuthorController.java +++ b/src/main/java/io/github/xxyopen/novel/controller/author/AuthorController.java @@ -1,25 +1,37 @@ 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.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +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 @@ -32,26 +44,92 @@ public class AuthorController { /** * 作家注册接口 */ + @Operation(summary = "作家注册接口") @PostMapping("register") public RestResp register(@Valid @RequestBody AuthorRegisterReqDto dto) { dto.setUserId(UserHolder.getUserId()); return authorService.register(dto); } + /** + * 查询作家状态接口 + */ + @Operation(summary = "作家状态查询接口") + @GetMapping("status") + public RestResp getStatus() { + return authorService.getStatus(UserHolder.getUserId()); + } + /** * 小说发布接口 */ + @Operation(summary = "小说发布接口") @PostMapping("book") public RestResp publishBook(@Valid @RequestBody BookAddReqDto dto) { return bookService.saveBook(dto); } + /** + * 小说发布列表查询接口 + */ + @Operation(summary = "小说发布列表查询接口") + @GetMapping("books") + public RestResp> listBooks(@ParameterObject PageReqDto dto) { + return bookService.listAuthorBooks(dto); + } + /** * 小说章节发布接口 */ - @PostMapping("book/chapter") - public RestResp publishBookChapter(@Valid @RequestBody ChapterAddReqDto dto) { + @Operation(summary = "小说章节发布接口") + @PostMapping("book/chapter/{bookId}") + public RestResp 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 deleteBookChapter( + @Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) { + return bookService.deleteBookChapter(chapterId); + } + + /** + * 小说章节查询接口 + */ + @Operation(summary = "小说章节查询接口") + @GetMapping("book/chapter/{chapterId}") + public RestResp getBookChapter( + @Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) { + return bookService.getBookChapter(chapterId); + } + + /** + * 小说章节更新接口 + */ + @Operation(summary = "小说章节更新接口") + @PutMapping("book/chapter/{chapterId}") + public RestResp 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> listBookChapters( + @Parameter(description = "小说ID") @PathVariable("bookId") Long bookId, + @ParameterObject PageReqDto dto) { + return bookService.listBookChapters(bookId, dto); + } + } diff --git a/src/main/java/io/github/xxyopen/novel/controller/front/BookController.java b/src/main/java/io/github/xxyopen/novel/controller/front/BookController.java index 46327cdd57bd7375d180463d239b228c992254ed..a299b286a41c5c0035caf37a289f06f238dd51c6 100644 --- a/src/main/java/io/github/xxyopen/novel/controller/front/BookController.java +++ b/src/main/java/io/github/xxyopen/novel/controller/front/BookController.java @@ -1,17 +1,26 @@ package io.github.xxyopen.novel.controller.front; -import io.github.xxyopen.novel.core.common.resp.PageRespDto; -import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.common.resp.RestResp; -import io.github.xxyopen.novel.dto.req.BookSearchReqDto; -import io.github.xxyopen.novel.dto.resp.*; +import io.github.xxyopen.novel.core.constant.ApiRouterConsts; +import io.github.xxyopen.novel.dto.resp.BookCategoryRespDto; +import io.github.xxyopen.novel.dto.resp.BookChapterAboutRespDto; +import io.github.xxyopen.novel.dto.resp.BookChapterRespDto; +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.SearchService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import java.security.NoSuchAlgorithmException; 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 控制器 @@ -19,6 +28,7 @@ import java.util.List; * @author xiongxiaoyang * @date 2022/5/14 */ +@Tag(name = "BookController", description = "前台门户-小说模块") @RestController @RequestMapping(ApiRouterConsts.API_FRONT_BOOK_URL_PREFIX) @RequiredArgsConstructor @@ -26,91 +36,99 @@ public class BookController { private final BookService bookService; - private final SearchService searchService; - /** * 小说分类列表查询接口 */ + @Operation(summary = "小说分类列表查询接口") @GetMapping("category/list") - public RestResp> listCategory(Integer workDirection) { + public RestResp> listCategory( + @Parameter(description = "作品方向", required = true) Integer workDirection) { return bookService.listCategory(workDirection); } - /** - * 小说搜索接口 - */ - @GetMapping("search_list") - public RestResp> searchBooks(BookSearchReqDto condition) { - return searchService.searchBooks(condition); - } - /** * 小说信息查询接口 */ + @Operation(summary = "小说信息查询接口") @GetMapping("{id}") - public RestResp getBookById(@PathVariable("id") Long bookId) { + public RestResp getBookById( + @Parameter(description = "小说 ID") @PathVariable("id") Long bookId) { return bookService.getBookById(bookId); } /** * 增加小说点击量接口 */ + @Operation(summary = "增加小说点击量接口") @PostMapping("visit") - public RestResp addVisitCount(Long bookId) { + public RestResp addVisitCount(@Parameter(description = "小说ID") Long bookId) { return bookService.addVisitCount(bookId); } /** * 小说最新章节相关信息查询接口 */ + @Operation(summary = "小说最新章节相关信息查询接口") @GetMapping("last_chapter/about") - public RestResp getLastChapterAbout(Long bookId) { + public RestResp getLastChapterAbout( + @Parameter(description = "小说ID") Long bookId) { return bookService.getLastChapterAbout(bookId); } /** * 小说推荐列表查询接口 */ + @Operation(summary = "小说推荐列表查询接口") @GetMapping("rec_list") - public RestResp> listRecBooks(Long bookId) throws NoSuchAlgorithmException { + public RestResp> listRecBooks( + @Parameter(description = "小说ID") Long bookId) throws NoSuchAlgorithmException { return bookService.listRecBooks(bookId); } /** * 小说章节列表查询接口 */ + @Operation(summary = "小说章节列表查询接口") @GetMapping("chapter/list") - public RestResp> listChapters(Long bookId) { + public RestResp> listChapters( + @Parameter(description = "小说ID") Long bookId) { return bookService.listChapters(bookId); } /** * 小说内容相关信息查询接口 */ + @Operation(summary = "小说内容相关信息查询接口") @GetMapping("content/{chapterId}") - public RestResp getBookContentAbout(@PathVariable("chapterId") Long chapterId) { + public RestResp getBookContentAbout( + @Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) { return bookService.getBookContentAbout(chapterId); } /** * 获取上一章节ID接口 */ + @Operation(summary = "获取上一章节ID接口") @GetMapping("pre_chapter_id/{chapterId}") - public RestResp getPreChapterId(@PathVariable("chapterId") Long chapterId) { + public RestResp getPreChapterId( + @Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) { return bookService.getPreChapterId(chapterId); } /** * 获取下一章节ID接口 */ + @Operation(summary = "获取下一章节ID接口") @GetMapping("next_chapter_id/{chapterId}") - public RestResp getNextChapterId(@PathVariable("chapterId") Long chapterId) { + public RestResp getNextChapterId( + @Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) { return bookService.getNextChapterId(chapterId); } /** * 小说点击榜查询接口 */ + @Operation(summary = "小说点击榜查询接口") @GetMapping("visit_rank") public RestResp> listVisitRankBooks() { return bookService.listVisitRankBooks(); @@ -119,6 +137,7 @@ public class BookController { /** * 小说新书榜查询接口 */ + @Operation(summary = "小说新书榜查询接口") @GetMapping("newest_rank") public RestResp> listNewestRankBooks() { return bookService.listNewestRankBooks(); @@ -127,6 +146,7 @@ public class BookController { /** * 小说更新榜查询接口 */ + @Operation(summary = "小说更新榜查询接口") @GetMapping("update_rank") public RestResp> listUpdateRankBooks() { return bookService.listUpdateRankBooks(); @@ -135,8 +155,10 @@ public class BookController { /** * 小说最新评论查询接口 */ + @Operation(summary = "小说最新评论查询接口") @GetMapping("comment/newest_list") - public RestResp listNewestComments(Long bookId) { + public RestResp listNewestComments( + @Parameter(description = "小说ID") Long bookId) { return bookService.listNewestComments(bookId); } diff --git a/src/main/java/io/github/xxyopen/novel/controller/front/HomeController.java b/src/main/java/io/github/xxyopen/novel/controller/front/HomeController.java index 9dddca2882148e63d32a34d15024b6d595d98d8d..bb8074ba87f1407941edf174792ff647d206d4ec 100644 --- a/src/main/java/io/github/xxyopen/novel/controller/front/HomeController.java +++ b/src/main/java/io/github/xxyopen/novel/controller/front/HomeController.java @@ -5,6 +5,8 @@ import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.dto.resp.HomeBookRespDto; import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto; 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 org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -18,6 +20,7 @@ import java.util.List; * @author xiongxiaoyang * @date 2022/5/12 */ +@Tag(name = "HomeController", description = "前台门户-首页模块") @RestController @RequestMapping(ApiRouterConsts.API_FRONT_HOME_URL_PREFIX) @RequiredArgsConstructor @@ -28,6 +31,7 @@ public class HomeController { /** * 首页小说推荐查询接口 */ + @Operation(summary = "首页小说推荐查询接口") @GetMapping("books") public RestResp> listHomeBooks() { return homeService.listHomeBooks(); @@ -36,6 +40,7 @@ public class HomeController { /** * 首页友情链接列表查询接口 */ + @Operation(summary = "首页友情链接列表查询接口") @GetMapping("friend_Link/list") public RestResp> listHomeFriendLinks() { return homeService.listHomeFriendLinks(); diff --git a/src/main/java/io/github/xxyopen/novel/controller/front/NewsController.java b/src/main/java/io/github/xxyopen/novel/controller/front/NewsController.java index db7bac3159850e34bae352a79c4685a14c1c5e67..e05716c3d5b6034fdfc1e9960967c234a0bfb913 100644 --- a/src/main/java/io/github/xxyopen/novel/controller/front/NewsController.java +++ b/src/main/java/io/github/xxyopen/novel/controller/front/NewsController.java @@ -1,23 +1,26 @@ package io.github.xxyopen.novel.controller.front; -import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.common.resp.RestResp; +import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto; 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 org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - /** * 前台门户-新闻模块 API 控制器 * * @author xiongxiaoyang * @date 2022/5/12 */ +@Tag(name = "NewsController", description = "前台门户-新闻模块") @RestController @RequestMapping(ApiRouterConsts.API_FRONT_NEWS_URL_PREFIX) @RequiredArgsConstructor @@ -28,6 +31,7 @@ public class NewsController { /** * 最新新闻列表查询接口 */ + @Operation(summary = "最新新闻列表查询接口") @GetMapping("latest_list") public RestResp> listLatestNews() { return newsService.listLatestNews(); @@ -36,8 +40,10 @@ public class NewsController { /** * 新闻信息查询接口 */ + @Operation(summary = "新闻信息查询接口") @GetMapping("{id}") - public RestResp getNews(@PathVariable Long id) { + public RestResp getNews( + @Parameter(description = "新闻ID") @PathVariable Long id) { return newsService.getNews(id); } } diff --git a/src/main/java/io/github/xxyopen/novel/controller/front/ResourceController.java b/src/main/java/io/github/xxyopen/novel/controller/front/ResourceController.java index edec67916a6e6547171b8e47dcebe5c774ccea63..93e173d59f64ebabfbb3cd9c9df847a33857a4c5 100644 --- a/src/main/java/io/github/xxyopen/novel/controller/front/ResourceController.java +++ b/src/main/java/io/github/xxyopen/novel/controller/front/ResourceController.java @@ -4,18 +4,25 @@ import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.dto.resp.ImgVerifyCodeRespDto; 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 org.springframework.web.bind.annotation.*; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - /** * 前台门户-资源(图片/视频/文档)模块 API 控制器 * * @author xiongxiaoyang * @date 2022/5/17 */ +@Tag(name = "ResourceController", description = "前台门户-资源模块") @RestController @RequestMapping(ApiRouterConsts.API_FRONT_RESOURCE_URL_PREFIX) @RequiredArgsConstructor @@ -26,6 +33,7 @@ public class ResourceController { /** * 获取图片验证码接口 */ + @Operation(summary = "获取图片验证码接口") @GetMapping("img_verify_code") public RestResp getImgVerifyCode() throws IOException { return resourceService.getImgVerifyCode(); @@ -33,9 +41,11 @@ public class ResourceController { /** * 图片上传接口 - * */ + */ + @Operation(summary = "图片上传接口") @PostMapping("/image") - RestResp uploadImage(@RequestParam("file") MultipartFile file) { + RestResp uploadImage( + @Parameter(description = "上传文件") @RequestParam("file") MultipartFile file) { return resourceService.uploadImage(file); } diff --git a/src/main/java/io/github/xxyopen/novel/controller/front/SearchController.java b/src/main/java/io/github/xxyopen/novel/controller/front/SearchController.java new file mode 100644 index 0000000000000000000000000000000000000000..ff3ee903285af11ae9cf9de57480d4c12620bacf --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/controller/front/SearchController.java @@ -0,0 +1,41 @@ +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> searchBooks( + @ParameterObject BookSearchReqDto condition) { + return searchService.searchBooks(condition); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/controller/front/UserController.java b/src/main/java/io/github/xxyopen/novel/controller/front/UserController.java index 39f597af17a5b571e04dcac602b7a480c7f074e0..27bef8566a8525ea86516108b84e0af928dfc65b 100644 --- a/src/main/java/io/github/xxyopen/novel/controller/front/UserController.java +++ b/src/main/java/io/github/xxyopen/novel/controller/front/UserController.java @@ -1,17 +1,25 @@ package io.github.xxyopen.novel.controller.front; 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.UserCommentReqDto; import io.github.xxyopen.novel.dto.req.UserInfoUptReqDto; import io.github.xxyopen.novel.dto.req.UserLoginReqDto; 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.UserRegisterRespDto; import io.github.xxyopen.novel.service.BookService; 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 lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -22,6 +30,8 @@ import org.springframework.web.bind.annotation.*; * @author xiongxiaoyang * @date 2022/5/17 */ +@Tag(name = "UserController", description = "前台门户-会员模块") +@SecurityRequirement(name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME) @RestController @RequestMapping(ApiRouterConsts.API_FRONT_USER_URL_PREFIX) @RequiredArgsConstructor @@ -34,6 +44,7 @@ public class UserController { /** * 用户注册接口 */ + @Operation(summary = "用户注册接口") @PostMapping("register") public RestResp register(@Valid @RequestBody UserRegisterReqDto dto) { return userService.register(dto); @@ -42,6 +53,7 @@ public class UserController { /** * 用户登录接口 */ + @Operation(summary = "用户登录接口") @PostMapping("login") public RestResp login(@Valid @RequestBody UserLoginReqDto dto) { return userService.login(dto); @@ -50,6 +62,7 @@ public class UserController { /** * 用户信息查询接口 */ + @Operation(summary = "用户信息查询接口") @GetMapping public RestResp getUserInfo() { return userService.getUserInfo(UserHolder.getUserId()); @@ -58,6 +71,7 @@ public class UserController { /** * 用户信息修改接口 */ + @Operation(summary = "用户信息修改接口") @PutMapping public RestResp updateUserInfo(@Valid @RequestBody UserInfoUptReqDto dto) { dto.setUserId(UserHolder.getUserId()); @@ -67,6 +81,7 @@ public class UserController { /** * 用户反馈提交接口 */ + @Operation(summary = "用户反馈提交接口") @PostMapping("feedback") public RestResp submitFeedback(@RequestBody String content) { return userService.saveFeedback(UserHolder.getUserId(), content); @@ -75,14 +90,16 @@ public class UserController { /** * 用户反馈删除接口 */ + @Operation(summary = "用户反馈删除接口") @DeleteMapping("feedback/{id}") - public RestResp deleteFeedback(@PathVariable Long id) { + public RestResp deleteFeedback(@Parameter(description = "反馈ID") @PathVariable Long id) { return userService.deleteFeedback(UserHolder.getUserId(), id); } /** * 发表评论接口 */ + @Operation(summary = "发表评论接口") @PostMapping("comment") public RestResp comment(@Valid @RequestBody UserCommentReqDto dto) { dto.setUserId(UserHolder.getUserId()); @@ -92,27 +109,38 @@ public class UserController { /** * 修改评论接口 */ + @Operation(summary = "修改评论接口") @PutMapping("comment/{id}") - public RestResp updateComment(@PathVariable Long id, String content) { + public RestResp updateComment(@Parameter(description = "评论ID") @PathVariable Long id, + String content) { return bookService.updateComment(UserHolder.getUserId(), id, content); } /** * 删除评论接口 */ + @Operation(summary = "删除评论接口") @DeleteMapping("comment/{id}") - public RestResp deleteComment(@PathVariable Long id) { + public RestResp deleteComment(@Parameter(description = "评论ID") @PathVariable Long id) { return bookService.deleteComment(UserHolder.getUserId(), id); } /** - * 查询书架状态接口 - * 0-不在书架 - * 1-已在书架 + * 查询书架状态接口 0-不在书架 1-已在书架 */ + @Operation(summary = "查询书架状态接口") @GetMapping("bookshelf_status") - public RestResp getBookshelfStatus(@RequestBody String bookId) { + public RestResp getBookshelfStatus(@Parameter(description = "小说ID") String bookId) { return userService.getBookshelfStatus(UserHolder.getUserId(), bookId); } + /** + * 分页查询评论 + */ + @Operation(summary = "查询会员评论列表接口") + @GetMapping("comments") + public RestResp> listComments(PageReqDto pageReqDto) { + return bookService.listComments(UserHolder.getUserId(), pageReqDto); + } + } diff --git a/src/main/java/io/github/xxyopen/novel/core/annotation/Key.java b/src/main/java/io/github/xxyopen/novel/core/annotation/Key.java new file mode 100644 index 0000000000000000000000000000000000000000..8f9812bb78dd0f5326ba1df7a9869bd4cdcac360 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/annotation/Key.java @@ -0,0 +1,23 @@ +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 ""; + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/annotation/Lock.java b/src/main/java/io/github/xxyopen/novel/core/annotation/Lock.java new file mode 100644 index 0000000000000000000000000000000000000000..a5e5be2b0e419f279d2e8afbc53f56f4352a4d6a --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/annotation/Lock.java @@ -0,0 +1,31 @@ +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; + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/aspect/LockAspect.java b/src/main/java/io/github/xxyopen/novel/core/aspect/LockAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..fbdecc704e600f7d8fe7768f172d2b902714c858 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/aspect/LockAspect.java @@ -0,0 +1,86 @@ +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); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/auth/AdminAuthStrategy.java b/src/main/java/io/github/xxyopen/novel/core/auth/AdminAuthStrategy.java index 88759cb206c315037bd738092edeae69ac4cf023..a68dc708f6dc2cbfae414d9778e1611cf8b7f2db 100644 --- a/src/main/java/io/github/xxyopen/novel/core/auth/AdminAuthStrategy.java +++ b/src/main/java/io/github/xxyopen/novel/core/auth/AdminAuthStrategy.java @@ -5,7 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; /** - * 平台后台管理系统 认证策略 + * 平台后台管理系统 认证授权策略 * * @author xiongxiaoyang * @date 2022/5/18 @@ -18,4 +18,5 @@ public class AdminAuthStrategy implements AuthStrategy { public void auth(String token, String requestUri) throws BusinessException { // TODO 平台后台 token 校验 } + } \ No newline at end of file diff --git a/src/main/java/io/github/xxyopen/novel/core/auth/AuthStrategy.java b/src/main/java/io/github/xxyopen/novel/core/auth/AuthStrategy.java index 8d394c8a7b83cf114b743064e13cd95945a3def7..758b1b0000778782ec22e142440056d0ef26f07e 100644 --- a/src/main/java/io/github/xxyopen/novel/core/auth/AuthStrategy.java +++ b/src/main/java/io/github/xxyopen/novel/core/auth/AuthStrategy.java @@ -6,9 +6,8 @@ import io.github.xxyopen.novel.core.constant.SystemConfigConsts; import io.github.xxyopen.novel.core.util.JwtUtils; import io.github.xxyopen.novel.dto.UserInfoDto; import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager; -import org.springframework.util.StringUtils; - import java.util.Objects; +import org.springframework.util.StringUtils; /** * 策略模式实现用户认证授权功能 @@ -19,23 +18,24 @@ import java.util.Objects; public interface AuthStrategy { /** - * 请求用户认证 + * 用户认证授权 * - * @param token 登录 token + * @param token 登录 token * @param requestUri 请求的 URI * @throws BusinessException 认证失败则抛出业务异常 */ void auth(String token, String requestUri) throws BusinessException; /** - * 前台多系统单点登录统一账号认证(门户系统、作家系统以及后面会扩展的漫画系统和视频系统等) + * 前台多系统单点登录统一账号认证授权(门户系统、作家系统以及后面会扩展的漫画系统和视频系统等) * * @param jwtUtils jwt 工具 * @param userInfoCacheManager 用户缓存管理对象 * @param token token 登录 token * @return 用户ID */ - default Long authSSO(JwtUtils jwtUtils, UserInfoCacheManager userInfoCacheManager, String token) { + default Long authSSO(JwtUtils jwtUtils, UserInfoCacheManager userInfoCacheManager, + String token) { if (!StringUtils.hasText(token)) { // token 为空 throw new BusinessException(ErrorCodeEnum.USER_LOGIN_EXPIRED); @@ -55,4 +55,5 @@ public interface AuthStrategy { // 返回 userId return userId; } + } diff --git a/src/main/java/io/github/xxyopen/novel/core/auth/AuthorAuthStrategy.java b/src/main/java/io/github/xxyopen/novel/core/auth/AuthorAuthStrategy.java index d6abcd6a0b218732444bc4b2f3d702c69971f8c3..f3e08271edd885d62e1bc0742c78b2de770a4e67 100644 --- a/src/main/java/io/github/xxyopen/novel/core/auth/AuthorAuthStrategy.java +++ b/src/main/java/io/github/xxyopen/novel/core/auth/AuthorAuthStrategy.java @@ -7,14 +7,13 @@ import io.github.xxyopen.novel.core.util.JwtUtils; import io.github.xxyopen.novel.dto.AuthorInfoDto; import io.github.xxyopen.novel.manager.cache.AuthorInfoCacheManager; import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - import java.util.List; import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; /** - * 作家后台管理系统 认证策略 + * 作家后台管理系统 认证授权策略 * * @author xiongxiaoyang * @date 2022/5/18 @@ -31,14 +30,17 @@ public class AuthorAuthStrategy implements AuthStrategy { /** * 不需要进行作家权限认证的 URI - * */ - private static final List EXCLUDE_URI = List.of(ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/register"); + */ + private static final List EXCLUDE_URI = List.of( + ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/register", + ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/status" + ); @Override public void auth(String token, String requestUri) throws BusinessException { // 统一账号认证 Long userId = authSSO(jwtUtils, userInfoCacheManager, token); - if(EXCLUDE_URI.contains(requestUri)){ + if (EXCLUDE_URI.contains(requestUri)) { // 该请求不需要进行作家权限认证 return; } @@ -52,4 +54,5 @@ public class AuthorAuthStrategy implements AuthStrategy { // 设置作家ID到当前线程 UserHolder.setAuthorId(authorInfo.getId()); } + } \ No newline at end of file diff --git a/src/main/java/io/github/xxyopen/novel/core/auth/FrontAuthStrategy.java b/src/main/java/io/github/xxyopen/novel/core/auth/FrontAuthStrategy.java index 2c69fb28506f3f441f5c293183dc8a34e8ed127a..214a747aec458d06bbac053b3b26e02c281bfe4a 100644 --- a/src/main/java/io/github/xxyopen/novel/core/auth/FrontAuthStrategy.java +++ b/src/main/java/io/github/xxyopen/novel/core/auth/FrontAuthStrategy.java @@ -7,7 +7,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; /** - * 前台门户系统 认证策略 + * 前台门户系统 认证授权策略 * * @author xiongxiaoyang * @date 2022/5/18 @@ -23,6 +23,7 @@ public class FrontAuthStrategy implements AuthStrategy { @Override public void auth(String token, String requestUri) throws BusinessException { // 统一账号认证 - authSSO(jwtUtils,userInfoCacheManager,token); + authSSO(jwtUtils, userInfoCacheManager, token); } + } \ No newline at end of file diff --git a/src/main/java/io/github/xxyopen/novel/core/auth/UserHolder.java b/src/main/java/io/github/xxyopen/novel/core/auth/UserHolder.java index 350d3db2eec43ebd68bb52d4153625511e821228..7ab877568674b8e5d439b68f7b57490d6f54fa58 100644 --- a/src/main/java/io/github/xxyopen/novel/core/auth/UserHolder.java +++ b/src/main/java/io/github/xxyopen/novel/core/auth/UserHolder.java @@ -13,12 +13,12 @@ public class UserHolder { /** * 当前线程用户ID - * */ + */ private static final ThreadLocal userIdTL = new ThreadLocal<>(); /** * 当前线程作家ID - * */ + */ private static final ThreadLocal authorIdTL = new ThreadLocal<>(); public void setUserId(Long userId) { @@ -37,7 +37,7 @@ public class UserHolder { return authorIdTL.get(); } - public void clear(){ + public void clear() { userIdTL.remove(); authorIdTL.remove(); } diff --git a/src/main/java/io/github/xxyopen/novel/core/common/constant/CommonConsts.java b/src/main/java/io/github/xxyopen/novel/core/common/constant/CommonConsts.java index e2fae0a4cc41ea315f193a215c98239b1ee05f5a..b943f28c6d75b14db493936e020321a73ea5f943 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/constant/CommonConsts.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/constant/CommonConsts.java @@ -11,33 +11,33 @@ public class CommonConsts { /** * 是 - * */ + */ public static final Integer YES = 1; public static final String TRUE = "true"; /** * 否 - * */ + */ public static final Integer NO = 0; public static final String FALSE = "false"; /** * 性别常量 - * */ - public enum SexEnum{ + */ + 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.desc = desc; } diff --git a/src/main/java/io/github/xxyopen/novel/core/common/constant/ErrorCodeEnum.java b/src/main/java/io/github/xxyopen/novel/core/common/constant/ErrorCodeEnum.java index e69e7d40a1ee598d3091f3e69be33d64cea5d190..918cf30556ea3ec88e2714da86d761efe8701c9b 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/constant/ErrorCodeEnum.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/constant/ErrorCodeEnum.java @@ -5,15 +5,12 @@ import lombok.Getter; /** * 错误码枚举类。 - * - * 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。 - * 错误产生来源分为 A/B/C, A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 - * 超时等问题; B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题; C 表示错误来源 - * 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的 + *

+ * 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。 错误产生来源分为 A/B/C, A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 超时等问题; B + * 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题; C 表示错误来源 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的 * 步长间距预留 100。 - * - * 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。 - * 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码。 + *

+ * 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码。 * * @author xiongxiaoyang * @date 2022/5/11 @@ -24,113 +21,132 @@ 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_ERROR("A0700", "用户上传文件异常"), /** * 用户上传文件类型不匹配 - * */ - USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701","用户上传文件类型不匹配"), + */ + 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 String code; + */ + private final String code; /** * 中文描述 - * */ - private String message; + */ + private final String message; } diff --git a/src/main/java/io/github/xxyopen/novel/core/common/exception/CommonExceptionHandler.java b/src/main/java/io/github/xxyopen/novel/core/common/exception/CommonExceptionHandler.java index 7b9e43f976c5d9349442742c41b4b2b372ff125d..3104c828ea0db066e3925e5b8100c795e2bfd0f0 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/exception/CommonExceptionHandler.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/exception/CommonExceptionHandler.java @@ -19,28 +19,28 @@ public class CommonExceptionHandler { /** * 处理数据校验异常 - * */ + */ @ExceptionHandler(BindException.class) - public RestResp handlerBindException(BindException e){ - log.error(e.getMessage(),e); + public RestResp handlerBindException(BindException e) { + log.error(e.getMessage(), e); return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR); } /** * 处理业务异常 - * */ + */ @ExceptionHandler(BusinessException.class) - public RestResp handlerBusinessException(BusinessException e){ - log.error(e.getMessage(),e); + public RestResp handlerBusinessException(BusinessException e) { + log.error(e.getMessage(), e); return RestResp.fail(e.getErrorCodeEnum()); } /** * 处理系统异常 - * */ + */ @ExceptionHandler(Exception.class) - public RestResp handlerException(Exception e){ - log.error(e.getMessage(),e); + public RestResp handlerException(Exception e) { + log.error(e.getMessage(), e); return RestResp.error(); } diff --git a/src/main/java/io/github/xxyopen/novel/core/common/req/PageReqDto.java b/src/main/java/io/github/xxyopen/novel/core/common/req/PageReqDto.java index 3d67116200211d69d1cd4e09909660fbd00d8da2..31c92c9b629446296b92ba7af60ae4a2b024adf6 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/req/PageReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/req/PageReqDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.core.common.req; +import io.swagger.v3.oas.annotations.Parameter; import lombok.Data; /** @@ -13,18 +14,20 @@ public class PageReqDto { /** * 请求页码,默认第 1 页 - * */ + */ + @Parameter(description = "请求页码,默认第 1 页") private int pageNum = 1; /** * 每页大小,默认每页 10 条 - * */ + */ + @Parameter(description = "每页大小,默认每页 10 条") private int pageSize = 10; /** - * 是否查询所有,默认不查所有 - * 为 true 时,pageNum 和 pageSize 无效 - * */ + * 是否查询所有,默认不查所有 为 true 时,pageNum 和 pageSize 无效 + */ + @Parameter(hidden = true) private boolean fetchAll = false; } diff --git a/src/main/java/io/github/xxyopen/novel/core/common/resp/PageRespDto.java b/src/main/java/io/github/xxyopen/novel/core/common/resp/PageRespDto.java index 65c954c1df667b58982a5080dd41453c0a0903bf..689fe6a24f507ee3e03738f9585f5ad010bef5d6 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/resp/PageRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/resp/PageRespDto.java @@ -1,8 +1,7 @@ package io.github.xxyopen.novel.core.common.resp; -import lombok.Getter; - import java.util.List; +import lombok.Getter; /** * 分页响应数据格式封装 @@ -34,8 +33,7 @@ public class PageRespDto { private final List list; /** - * 该构造函数用于通用分页查询的场景 - * 接收普通分页数据和普通集合 + * 该构造函数用于通用分页查询的场景 接收普通分页数据和普通集合 */ public PageRespDto(long pageNum, long pageSize, long total, List list) { this.pageNum = pageNum; @@ -50,7 +48,7 @@ public class PageRespDto { /** * 获取分页数 - * */ + */ public long getPages() { if (this.pageSize == 0L) { return 0L; @@ -59,7 +57,6 @@ public class PageRespDto { if (this.total % this.pageSize != 0L) { ++pages; } - return pages; } } diff --git a/src/main/java/io/github/xxyopen/novel/core/common/resp/RestResp.java b/src/main/java/io/github/xxyopen/novel/core/common/resp/RestResp.java index f07af5ebbf7a8e648074932ac644c6730f38a4f2..3a6ee8e44017bd32bc3b01dae926c476fa393e16 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/resp/RestResp.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/resp/RestResp.java @@ -1,6 +1,7 @@ package io.github.xxyopen.novel.core.common.resp; import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import java.util.Objects; @@ -17,16 +18,19 @@ public class RestResp { /** * 响应码 */ + @Schema(description = "错误码,00000-没有错误") private String code; /** * 响应消息 */ + @Schema(description = "响应消息") private String message; /** * 响应数据 */ + @Schema(description = "响应数据") private T data; private RestResp() { diff --git a/src/main/java/io/github/xxyopen/novel/core/common/util/ImgVerifyCodeUtils.java b/src/main/java/io/github/xxyopen/novel/core/common/util/ImgVerifyCodeUtils.java index 073a3cba1ac3d53370176767bf38fd290238a804..2e230d4bdd968be64a2e43ed70846b2114338bf1 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/util/ImgVerifyCodeUtils.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/util/ImgVerifyCodeUtils.java @@ -1,14 +1,15 @@ package io.github.xxyopen.novel.core.common.util; -import lombok.experimental.UtilityClass; - -import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Base64; import java.util.Random; +import javax.imageio.ImageIO; +import lombok.experimental.UtilityClass; /** * 图片验证码工具类 @@ -79,7 +80,7 @@ public class ImgVerifyCodeUtils { for (int i = 1; i <= verifyCode.length(); i++) { g.setFont(getFont()); g.setColor(new Color(random.nextInt(101), random.nextInt(111), random - .nextInt(121))); + .nextInt(121))); g.translate(random.nextInt(3), random.nextInt(3)); g.drawString(String.valueOf(verifyCode.charAt(i - 1)), 13 * i, 23); } diff --git a/src/main/java/io/github/xxyopen/novel/core/common/util/IpUtils.java b/src/main/java/io/github/xxyopen/novel/core/common/util/IpUtils.java index 6d5f2f20337eb6c793d545cf365307d178c4341d..0d628c8d5bc539499ec961fd0905cddb5f12702e 100644 --- a/src/main/java/io/github/xxyopen/novel/core/common/util/IpUtils.java +++ b/src/main/java/io/github/xxyopen/novel/core/common/util/IpUtils.java @@ -16,6 +16,7 @@ public class IpUtils { /** * 获取真实IP + * * @return 真实IP */ public String getRealIp(HttpServletRequest request) { diff --git a/src/main/java/io/github/xxyopen/novel/core/config/CacheConfig.java b/src/main/java/io/github/xxyopen/novel/core/config/CacheConfig.java index 8a13fbe707e611958ee260db13a9a31111b1c60b..ac6a643c03cb8eba7ba16d2e8b8fa673c695b246 100644 --- a/src/main/java/io/github/xxyopen/novel/core/config/CacheConfig.java +++ b/src/main/java/io/github/xxyopen/novel/core/config/CacheConfig.java @@ -2,6 +2,11 @@ package io.github.xxyopen.novel.core.config; import com.github.benmanes.caffeine.cache.Caffeine; 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.caffeine.CaffeineCache; import org.springframework.cache.support.SimpleCacheManager; @@ -13,12 +18,6 @@ import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; 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; - /** * 缓存配置类 * @@ -37,9 +36,11 @@ public class CacheConfig { SimpleCacheManager cacheManager = new SimpleCacheManager(); List caches = new ArrayList<>(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.isLocal()) { - Caffeine caffeine = Caffeine.newBuilder().recordStats().maximumSize(c.getMaxSize()); + Caffeine caffeine = Caffeine.newBuilder().recordStats() + .maximumSize(c.getMaxSize()); if (c.getTtl() > 0) { caffeine.expireAfterWrite(Duration.ofSeconds(c.getTtl())); } @@ -56,25 +57,32 @@ public class CacheConfig { */ @Bean public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) { - RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); + RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter( + connectionFactory); RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() - .disableCachingNullValues().prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX); + .disableCachingNullValues().prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX); - Map cacheMap = new LinkedHashMap<>(CacheConsts.CacheEnum.values().length); - for (CacheConsts.CacheEnum c : CacheConsts.CacheEnum.values()) { + Map cacheMap = new LinkedHashMap<>( + CacheConsts.CacheEnum.values().length); + // 类型推断 var 非常适合 for 循环,JDK 10 引入,JDK 11 改进 + for (var c : CacheConsts.CacheEnum.values()) { if (c.isRemote()) { if (c.getTtl() > 0) { - cacheMap.put(c.getName(), RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues() - .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX).entryTtl(Duration.ofSeconds(c.getTtl()))); + cacheMap.put(c.getName(), + RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues() + .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX) + .entryTtl(Duration.ofSeconds(c.getTtl()))); } else { - cacheMap.put(c.getName(), RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues() + cacheMap.put(c.getName(), + RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues() .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX)); } } } - RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig, cacheMap); + RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter, + defaultCacheConfig, cacheMap); redisCacheManager.setTransactionAware(true); redisCacheManager.initializeCaches(); return redisCacheManager; diff --git a/src/main/java/io/github/xxyopen/novel/core/config/CorsConfig.java b/src/main/java/io/github/xxyopen/novel/core/config/CorsConfig.java index a14e4976130c7a3468c7a0ee78e69c0cf0a72ae1..1346aa717879e4ba26cab5eb16026985c2c60da1 100644 --- a/src/main/java/io/github/xxyopen/novel/core/config/CorsConfig.java +++ b/src/main/java/io/github/xxyopen/novel/core/config/CorsConfig.java @@ -25,7 +25,7 @@ public class CorsConfig { public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 允许的域,不要写*,否则cookie就无法使用了 - for (String allowOrigin : corsProperties.getAllowOrigins()) { + for (String allowOrigin : corsProperties.allowOrigins()) { config.addAllowedOrigin(allowOrigin); } // 允许的头信息 @@ -37,7 +37,7 @@ public class CorsConfig { UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource(); // 添加映射路径,拦截一切请求 - configurationSource.registerCorsConfiguration("/**",config); + configurationSource.registerCorsConfiguration("/**", config); return new CorsFilter(configurationSource); } diff --git a/src/main/java/io/github/xxyopen/novel/core/config/CorsProperties.java b/src/main/java/io/github/xxyopen/novel/core/config/CorsProperties.java index 51f5cfe211dc1382a72b917287cbcc053ee46a06..7f3f8f7047a8a3d674e5e8fbeb79f4ccd8536291 100644 --- a/src/main/java/io/github/xxyopen/novel/core/config/CorsProperties.java +++ b/src/main/java/io/github/xxyopen/novel/core/config/CorsProperties.java @@ -1,6 +1,5 @@ package io.github.xxyopen.novel.core.config; -import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.List; @@ -12,11 +11,6 @@ import java.util.List; * @date 2022/5/17 */ @ConfigurationProperties(prefix = "novel.cors") -@Data -public class CorsProperties { +public record CorsProperties(List allowOrigins) { - /** - * 允许跨域的域名 - * */ - private List allowOrigins; } diff --git a/src/main/java/io/github/xxyopen/novel/core/config/EsConfig.java b/src/main/java/io/github/xxyopen/novel/core/config/EsConfig.java index 0603fe40a979d8439e67b18a6ef280e4bd455e9a..38620b259f241f5d1ee3def7cc339d244c598033 100644 --- a/src/main/java/io/github/xxyopen/novel/core/config/EsConfig.java +++ b/src/main/java/io/github/xxyopen/novel/core/config/EsConfig.java @@ -1,35 +1,73 @@ package io.github.xxyopen.novel.core.config; -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.json.jackson.JacksonJsonpMapper; -import co.elastic.clients.transport.ElasticsearchTransport; -import co.elastic.clients.transport.rest_client.RestClientTransport; -import lombok.RequiredArgsConstructor; +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 相关配置 + * Elasticsearch 相关配置 * * @author xiongxiaoyang * @date 2022/5/23 */ @Configuration -@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true") -@RequiredArgsConstructor +@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 - public ElasticsearchClient elasticsearchClient(RestClient restClient) { + RestClient elasticsearchRestClient(RestClientBuilder restClientBuilder, + ObjectProvider 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) { + + } - // Create the transport with a Jackson mapper - ElasticsearchTransport transport = new RestClientTransport( - restClient, new JacksonJsonpMapper()); + @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); - // And create the API client - return new ElasticsearchClient(transport); + builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(clientBuilder)); + return clientBuilder; + }); + return restClientBuilder.build(); } } diff --git a/src/main/java/io/github/xxyopen/novel/core/config/MailProperties.java b/src/main/java/io/github/xxyopen/novel/core/config/MailProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..184990314a6aff3dc734347b2987d92a6168cb76 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/config/MailProperties.java @@ -0,0 +1,14 @@ +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) { + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/config/OpenApiConfig.java b/src/main/java/io/github/xxyopen/novel/core/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..c66b99842a54505ed2b8476a492d61d8665630f6 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/config/OpenApiConfig.java @@ -0,0 +1,25 @@ +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 { + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/config/ShardingSphereConfiguration.java b/src/main/java/io/github/xxyopen/novel/core/config/ShardingSphereConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..ec465efe5acb476226dc69d1cca32a2fda84575b --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/config/ShardingSphereConfiguration.java @@ -0,0 +1,24 @@ +package io.github.xxyopen.novel.core.config; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +/** + * ShardingSphere 配置类,控制是否开启 ShardingSphere + * + * @author xiongxiaoyang + * @date 2023/12/21 + */ +@Configuration +@EnableAutoConfiguration(exclude = { + org.apache.shardingsphere.spring.boot.ShardingSphereAutoConfiguration.class +}) +@ConditionalOnProperty( + prefix = "spring.shardingsphere", + name = {"enabled"}, + havingValue = "false" +) +public class ShardingSphereConfiguration { + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/config/WebConfig.java b/src/main/java/io/github/xxyopen/novel/core/config/WebConfig.java index 2bb0fae487c000f107c3f94154c40af54891e28c..e01823d7331d7f9eafed3765ae1733fec8701d2e 100644 --- a/src/main/java/io/github/xxyopen/novel/core/config/WebConfig.java +++ b/src/main/java/io/github/xxyopen/novel/core/config/WebConfig.java @@ -4,15 +4,16 @@ 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.FileInterceptor; +import io.github.xxyopen.novel.core.interceptor.FlowLimitInterceptor; +import io.github.xxyopen.novel.core.interceptor.TokenParseInterceptor; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** - * Spring Web Mvc 相关配置 - * 不要加 @EnableWebMvc 注解,否则会导致 jackson 的全局配置失效 - * 类上添加 @EnableWebMvc 会导致 WebMvcAutoConfiguration 中的自动配置全部失效 + * Spring Web Mvc 相关配置不要加 @EnableWebMvc 注解,否则会导致 jackson 的全局配置失效。因为 @EnableWebMvc 注解会导致 + * WebMvcAutoConfiguration 自动配置失效 * * @author xiongxiaoyang * @date 2022/5/18 @@ -21,28 +22,46 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + private final FlowLimitInterceptor flowLimitInterceptor; + private final AuthInterceptor authInterceptor; private final FileInterceptor fileInterceptor; + private final TokenParseInterceptor tokenParseInterceptor; + @Override public void addInterceptors(InterceptorRegistry registry) { + + // 流量限制拦截器 + registry.addInterceptor(flowLimitInterceptor) + .addPathPatterns("/**") + .order(0); + // 文件访问拦截 registry.addInterceptor(fileInterceptor) - .addPathPatterns(SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY + "**"); + .addPathPatterns(SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY + "**") + .order(1); // 权限认证拦截 registry.addInterceptor(authInterceptor) - // 拦截会员中心相关请求接口 - .addPathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/**" - // 拦截作家后台相关请求接口 - , ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/**" - // 拦截平台后台相关请求接口 - , ApiRouterConsts.API_ADMIN_URL_PREFIX + "/**") - // 放行登录注册相关请求接口 - .excludePathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/register" - , ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/login" - ,ApiRouterConsts.API_ADMIN_URL_PREFIX + "/login"); + // 拦截会员中心相关请求接口 + .addPathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/**", + // 拦截作家后台相关请求接口 + ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/**", + // 拦截平台后台相关请求接口 + ApiRouterConsts.API_ADMIN_URL_PREFIX + "/**") + // 放行登录注册相关请求接口 + .excludePathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/register", + ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/login", + ApiRouterConsts.API_ADMIN_URL_PREFIX + "/login") + .order(2); + + // Token 解析拦截器 + registry.addInterceptor(tokenParseInterceptor) + // 拦截小说内容查询接口,需要解析 token 以判断该用户是否有权阅读该章节(付费章节是否已购买) + .addPathPatterns(ApiRouterConsts.API_FRONT_BOOK_URL_PREFIX + "/content/*") + .order(3); } } diff --git a/src/main/java/io/github/xxyopen/novel/core/config/XssProperties.java b/src/main/java/io/github/xxyopen/novel/core/config/XssProperties.java index 613e0906313c995cf298f875add74c49b78e6da2..e6d814ee124e1f4eba07c223a415051703dd588c 100644 --- a/src/main/java/io/github/xxyopen/novel/core/config/XssProperties.java +++ b/src/main/java/io/github/xxyopen/novel/core/config/XssProperties.java @@ -1,9 +1,7 @@ package io.github.xxyopen.novel.core.config; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; - import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; /** * Xss 过滤配置属性 @@ -12,17 +10,6 @@ import java.util.List; * @date 2022/5/17 */ @ConfigurationProperties(prefix = "novel.xss") -@Data -public class XssProperties { - - /** - * 过滤开关 - * */ - private Boolean enabled; - - /** - * 排除链接 - * */ - private List excludes; +public record XssProperties(Boolean enabled, List excludes) { } diff --git a/src/main/java/io/github/xxyopen/novel/core/config/XxlJobConfig.java b/src/main/java/io/github/xxyopen/novel/core/config/XxlJobConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5d87be1dad2c30f7110463da4a04f34da2cc8272 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/config/XxlJobConfig.java @@ -0,0 +1,44 @@ +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; + } + +} \ No newline at end of file diff --git a/src/main/java/io/github/xxyopen/novel/core/constant/AmqpConsts.java b/src/main/java/io/github/xxyopen/novel/core/constant/AmqpConsts.java index 7292bdadfde2db464ff1c643cbd9324b31ba1a99..7ccf8612acfeae20cd1fd2be7ccbc25726baf788 100644 --- a/src/main/java/io/github/xxyopen/novel/core/constant/AmqpConsts.java +++ b/src/main/java/io/github/xxyopen/novel/core/constant/AmqpConsts.java @@ -10,22 +10,22 @@ public class AmqpConsts { /** * 小说信息改变 MQ - * */ - public static class BookChangeMq{ + */ + 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"; } diff --git a/src/main/java/io/github/xxyopen/novel/core/constant/ApiRouterConsts.java b/src/main/java/io/github/xxyopen/novel/core/constant/ApiRouterConsts.java index 881f95f8c276dfe1d6f38f6a188ef8b94a2ae8ff..4a626102478150fe5d82d56df7b558ef1f122f33 100644 --- a/src/main/java/io/github/xxyopen/novel/core/constant/ApiRouterConsts.java +++ b/src/main/java/io/github/xxyopen/novel/core/constant/ApiRouterConsts.java @@ -34,29 +34,34 @@ public class ApiRouterConsts { /** * 首页模块请求路径前缀 - * */ + */ public static final String HOME_URL_PREFIX = "/home"; /** * 首页模块请求路径前缀 - * */ + */ public static final String NEWS_URL_PREFIX = "/news"; /** * 小说模块请求路径前缀 - * */ + */ public static final String BOOK_URL_PREFIX = "/book"; /** * 会员模块请求路径前缀 - * */ + */ public static final String USER_URL_PREFIX = "/user"; /** * 资源(图片/视频/文档)模块请求路径前缀 - * */ + */ public static final String RESOURCE_URL_PREFIX = "/resource"; + /** + * 搜索模块请求路径前缀 + */ + public static final String SEARCH_URL_PREFIX = "/search"; + /** * 前台门户首页API请求路径前缀 */ @@ -80,6 +85,13 @@ public class ApiRouterConsts { /** * 前台门户资源(图片/视频/文档)相关API请求路径前缀 */ - public static final String API_FRONT_RESOURCE_URL_PREFIX = API_FRONT_URL_PREFIX + RESOURCE_URL_PREFIX; + public static final String API_FRONT_RESOURCE_URL_PREFIX = + API_FRONT_URL_PREFIX + RESOURCE_URL_PREFIX; + + /** + * 前台门户搜索相关API请求路径前缀 + */ + public static final String API_FRONT_SEARCH_URL_PREFIX = + API_FRONT_URL_PREFIX + SEARCH_URL_PREFIX; } diff --git a/src/main/java/io/github/xxyopen/novel/core/constant/CacheConsts.java b/src/main/java/io/github/xxyopen/novel/core/constant/CacheConsts.java index d7eabd452cfd33c99e4ce7a14c6932286fc4ff8a..f5164c944f4130263031b148e8e06a530b7343a4 100644 --- a/src/main/java/io/github/xxyopen/novel/core/constant/CacheConsts.java +++ b/src/main/java/io/github/xxyopen/novel/core/constant/CacheConsts.java @@ -56,7 +56,7 @@ public class CacheConsts { /** * 小说分类列表缓存 - * */ + */ public static final String BOOK_CATEGORY_LIST_CACHE_NAME = "bookCategoryListCache"; /** @@ -76,13 +76,14 @@ public class CacheConsts { /** * 最近更新小说ID列表缓存 - * */ + */ public static final String LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME = "lastUpdateBookIdListCache"; /** * 图片验证码缓存 KEY - * */ - public static final String IMG_VERIFY_CODE_CACHE_KEY = REDIS_CACHE_PREFIX + "imgVerifyCodeCache::"; + */ + public static final String IMG_VERIFY_CODE_CACHE_KEY = + REDIS_CACHE_PREFIX + "imgVerifyCodeCache::"; /** * 用户信息缓存 @@ -111,19 +112,19 @@ public class CacheConsts { 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_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), - 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-远程 diff --git a/src/main/java/io/github/xxyopen/novel/core/constant/DatabaseConsts.java b/src/main/java/io/github/xxyopen/novel/core/constant/DatabaseConsts.java index 49533d755e700f961e5ae791bea42e53925e412b..32c89f4fe688e98ec71925fd58797e2e43c2f95a 100644 --- a/src/main/java/io/github/xxyopen/novel/core/constant/DatabaseConsts.java +++ b/src/main/java/io/github/xxyopen/novel/core/constant/DatabaseConsts.java @@ -10,7 +10,6 @@ import lombok.Getter; */ public class DatabaseConsts { - /** * 用户信息表 */ @@ -89,8 +88,14 @@ public class DatabaseConsts { public static final String COLUMN_CATEGORY_ID = "category_id"; + public static final String COLUMN_BOOK_NAME = "book_name"; + + public static final String AUTHOR_ID = "author_id"; + public static final String COLUMN_VISIT_COUNT = "visit_count"; + public static final String COLUMN_WORD_COUNT = "word_count"; + public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time"; } diff --git a/src/main/java/io/github/xxyopen/novel/core/constant/EsConsts.java b/src/main/java/io/github/xxyopen/novel/core/constant/EsConsts.java index 5527098e0dcb9d30f8cbef62bdc88ede58202a3b..da8c52d0e3639118edcb38548e728e5c06371bc3 100644 --- a/src/main/java/io/github/xxyopen/novel/core/constant/EsConsts.java +++ b/src/main/java/io/github/xxyopen/novel/core/constant/EsConsts.java @@ -14,8 +14,8 @@ public class EsConsts { /** * 小说索引 - * */ - public static class BookIndex{ + */ + public static class BookIndex { private BookIndex() { throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG); @@ -23,18 +23,18 @@ public class EsConsts { /** * 索引名 - * */ + */ public static final String INDEX_NAME = "book"; /** * id */ - public static final String FIELD_ID = "id"; + public static final String FIELD_ID = "id"; /** * 作品方向;0-男频 1-女频 */ - public static final String FIELD_WORK_DIRECTION = "workDirection"; + public static final String FIELD_WORK_DIRECTION = "workDirection"; /** * 类别ID @@ -110,7 +110,7 @@ public class EsConsts { * 是否收费;1-收费 0-免费 */ public static final String FIELD_IS_VIP = "isVip"; - + } } diff --git a/src/main/java/io/github/xxyopen/novel/core/constant/MessageSenderTypeConsts.java b/src/main/java/io/github/xxyopen/novel/core/constant/MessageSenderTypeConsts.java new file mode 100644 index 0000000000000000000000000000000000000000..8cbc25f38dd8b7679a6ced2ab061c28af6dca9f5 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/constant/MessageSenderTypeConsts.java @@ -0,0 +1,25 @@ +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"; + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/constant/SystemConfigConsts.java b/src/main/java/io/github/xxyopen/novel/core/constant/SystemConfigConsts.java index 3d7b94d9ba3862412407d795f00fec3518aedc82..858aefa920a069eb1e33b1fdc00d04f48b1d46da 100644 --- a/src/main/java/io/github/xxyopen/novel/core/constant/SystemConfigConsts.java +++ b/src/main/java/io/github/xxyopen/novel/core/constant/SystemConfigConsts.java @@ -14,32 +14,32 @@ public class SystemConfigConsts { /** * Http 请求认证 Header - * */ + */ public static final String HTTP_AUTH_HEADER_NAME = "Authorization"; /** * 前台门户系统标识 - * */ + */ public static final String NOVEL_FRONT_KEY = "front"; /** * 作家管理系统标识 - * */ + */ public static final String NOVEL_AUTHOR_KEY = "author"; /** * 后台管理系统标识 - * */ + */ public static final String NOVEL_ADMIN_KEY = "admin"; /** * 图片上传目录 - * */ + */ public static final String IMAGE_UPLOAD_DIRECTORY = "/image/"; /** * 常量类实例化异常信息 - * */ + */ public static final String CONST_INSTANCE_EXCEPTION_MSG = "Constant class"; } diff --git a/src/main/java/io/github/xxyopen/novel/core/filter/XssFilter.java b/src/main/java/io/github/xxyopen/novel/core/filter/XssFilter.java index b1f3942d7d232574a92378b8e72024c51a2ec242..709cab656d8cb00eb6c39f7f5405ded22ae282d5 100644 --- a/src/main/java/io/github/xxyopen/novel/core/filter/XssFilter.java +++ b/src/main/java/io/github/xxyopen/novel/core/filter/XssFilter.java @@ -3,18 +3,22 @@ package io.github.xxyopen.novel.core.filter; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import io.github.xxyopen.novel.core.config.XssProperties; import io.github.xxyopen.novel.core.wrapper.XssHttpServletRequestWrapper; -import jakarta.servlet.*; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * 防止 XSS 攻击的过滤器 * @@ -22,13 +26,12 @@ import java.util.regex.Pattern; * @date 2022/5/17 */ @Component -@ConditionalOnProperty(value = "novel.xss.enabled",havingValue = "true") +@ConditionalOnProperty(value = "novel.xss.enabled", havingValue = "true") @WebFilter(urlPatterns = "/*", filterName = "xssFilter") @EnableConfigurationProperties(value = {XssProperties.class}) @RequiredArgsConstructor public class XssFilter implements Filter { - private final XssProperties xssProperties; @Override @@ -37,22 +40,24 @@ public class XssFilter implements Filter { } @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (handleExcludeUrl(req)) { filterChain.doFilter(servletRequest, servletResponse); return; } - XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) servletRequest); + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper( + (HttpServletRequest) servletRequest); filterChain.doFilter(xssRequest, servletResponse); } private boolean handleExcludeUrl(HttpServletRequest request) { - if (CollectionUtils.isEmpty(xssProperties.getExcludes())) { + if (CollectionUtils.isEmpty(xssProperties.excludes())) { return false; } String url = request.getServletPath(); - for (String pattern : xssProperties.getExcludes()) { + for (String pattern : xssProperties.excludes()) { Pattern p = Pattern.compile("^" + pattern); Matcher m = p.matcher(url); if (m.find()) { diff --git a/src/main/java/io/github/xxyopen/novel/core/interceptor/AuthInterceptor.java b/src/main/java/io/github/xxyopen/novel/core/interceptor/AuthInterceptor.java index 79b5566d32b3b679869cfbbffdcf63460a27874e..2f16f58becee4ca94410484513013d36ac7f8a23 100644 --- a/src/main/java/io/github/xxyopen/novel/core/interceptor/AuthInterceptor.java +++ b/src/main/java/io/github/xxyopen/novel/core/interceptor/AuthInterceptor.java @@ -19,8 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.Map; /** - * 认证 拦截器 - * 为了注入其它的 Spring beans,需要通过 @Component 注解将该拦截器注册到 Spring 上下文 + * 认证授权 拦截器:为了注入其它的 Spring beans,需要通过 @Component 注解将该拦截器注册到 Spring 上下文 * * @author xiongxiaoyang * @date 2022/5/18 @@ -29,13 +28,17 @@ import java.util.Map; @RequiredArgsConstructor public class AuthInterceptor implements HandlerInterceptor { - private final Map authStrategy; + private final Map authStrategy; private final ObjectMapper objectMapper; + /** + * handle 执行前调用 + */ @SuppressWarnings("NullableProblems") @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { // 获取登录 JWT String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME); @@ -44,27 +47,43 @@ public class AuthInterceptor implements HandlerInterceptor { // 根据请求的 URI 得到认证策略 String subUri = requestUri.substring(ApiRouterConsts.API_URL_PREFIX.length() + 1); - String systemName = subUri.substring(0,subUri.indexOf("/")); - String authStrategyName = String.format("%sAuthStrategy",systemName); + String systemName = subUri.substring(0, subUri.indexOf("/")); + String authStrategyName = String.format("%sAuthStrategy", systemName); // 开始认证 try { - authStrategy.get(authStrategyName).auth(token,requestUri); + authStrategy.get(authStrategyName).auth(token, requestUri); return HandlerInterceptor.super.preHandle(request, response, handler); - }catch (BusinessException exception){ + } catch (BusinessException exception) { // 认证失败 response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.getWriter().write(objectMapper.writeValueAsString(RestResp.fail(exception.getErrorCodeEnum()))); + response.getWriter().write( + objectMapper.writeValueAsString(RestResp.fail(exception.getErrorCodeEnum()))); return false; } } + /** + * handler 执行后调用,出现异常不调用 + */ @SuppressWarnings("NullableProblems") @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + 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(); - HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); + HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } + } diff --git a/src/main/java/io/github/xxyopen/novel/core/interceptor/FileInterceptor.java b/src/main/java/io/github/xxyopen/novel/core/interceptor/FileInterceptor.java index 5121950a0b86cd07bd40b3f4a55ee1c54c9395b9..e7317bb27f4d87e6048519f4a53c1e83311d2b7c 100644 --- a/src/main/java/io/github/xxyopen/novel/core/interceptor/FileInterceptor.java +++ b/src/main/java/io/github/xxyopen/novel/core/interceptor/FileInterceptor.java @@ -2,15 +2,14 @@ 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; -import java.io.FileInputStream; -import java.io.InputStream; -import java.io.OutputStream; - /** * 文件 拦截器 * @@ -26,12 +25,14 @@ public class FileInterceptor implements HandlerInterceptor { @SuppressWarnings("NullableProblems") @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + 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)) { + 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); diff --git a/src/main/java/io/github/xxyopen/novel/core/interceptor/FlowLimitInterceptor.java b/src/main/java/io/github/xxyopen/novel/core/interceptor/FlowLimitInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..7afe8724492bc0f160e0d1d50b0390b96ef128e8 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/interceptor/FlowLimitInterceptor.java @@ -0,0 +1,98 @@ +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 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; + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/interceptor/TokenParseInterceptor.java b/src/main/java/io/github/xxyopen/novel/core/interceptor/TokenParseInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..9642f84829831bfded95d2b5d4184f4000ccd1cd --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/interceptor/TokenParseInterceptor.java @@ -0,0 +1,50 @@ +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); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/json/deserializer/GlobalJsonDeserializer.java b/src/main/java/io/github/xxyopen/novel/core/json/deserializer/GlobalJsonDeserializer.java index b20d046303eb4184f5dfea1eb8b458a6cb9fa38b..83476c8f4b1823b1f86d4df55ef66663d29883c5 100644 --- a/src/main/java/io/github/xxyopen/novel/core/json/deserializer/GlobalJsonDeserializer.java +++ b/src/main/java/io/github/xxyopen/novel/core/json/deserializer/GlobalJsonDeserializer.java @@ -3,9 +3,8 @@ 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 org.springframework.boot.jackson.JsonComponent; - import java.io.IOException; +import org.springframework.boot.jackson.JsonComponent; /** @@ -18,16 +17,16 @@ import java.io.IOException; public class GlobalJsonDeserializer { /** - * 字符串反序列化器 - * 过滤特殊字符,解决 XSS 攻击 + * 字符串反序列化器:过滤特殊字符,解决 XSS 攻击 */ public static class StringDeserializer extends JsonDeserializer { @Override - public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + public String deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext) throws IOException { return jsonParser.getValueAsString() - .replace("<", "<") - .replace(">", ">"); + .replace("<", "<") + .replace(">", ">"); } } } diff --git a/src/main/java/io/github/xxyopen/novel/core/json/serializer/UsernameSerializer.java b/src/main/java/io/github/xxyopen/novel/core/json/serializer/UsernameSerializer.java index e2a1a9ad5cfca448e2fed8abfc78d373188e0428..d14c25ac87e24e15f47743f4ac9a52c9e474f6a7 100644 --- a/src/main/java/io/github/xxyopen/novel/core/json/serializer/UsernameSerializer.java +++ b/src/main/java/io/github/xxyopen/novel/core/json/serializer/UsernameSerializer.java @@ -3,7 +3,6 @@ package io.github.xxyopen.novel.core.json.serializer; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; - import java.io.IOException; /** @@ -15,8 +14,9 @@ import java.io.IOException; public class UsernameSerializer extends JsonSerializer { @Override - public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeString(s.substring(0,4) + "****" + s.substring(8)); + public void serialize(String s, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeString(s.substring(0, 4) + "****" + s.substring(8)); } } diff --git a/src/main/java/io/github/xxyopen/novel/core/listener/RabbitQueueListener.java b/src/main/java/io/github/xxyopen/novel/core/listener/RabbitQueueListener.java index 55d82f2bb96f52ba915c8bbcf155f1e348b6e368..2189bef60010f72667eb6b59865bb315da3cdba9 100644 --- a/src/main/java/io/github/xxyopen/novel/core/listener/RabbitQueueListener.java +++ b/src/main/java/io/github/xxyopen/novel/core/listener/RabbitQueueListener.java @@ -21,7 +21,8 @@ import org.springframework.stereotype.Component; * @date 2022/5/25 */ @Component -@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true") +@ConditionalOnProperty(prefix = "spring", name = {"elasticsearch.enabled", + "amqp.enabled"}, havingValue = "true") @RequiredArgsConstructor @Slf4j public class RabbitQueueListener { @@ -32,15 +33,15 @@ public class RabbitQueueListener { /** * 监听小说信息改变的 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)) + .index(EsConsts.BookIndex.INDEX_NAME) + .id(bookInfo.getId().toString()) + .document(EsBookDto.build(bookInfo)) ); log.info("Indexed with version " + response.version()); } diff --git a/src/main/java/io/github/xxyopen/novel/core/task/BookToEsTask.java b/src/main/java/io/github/xxyopen/novel/core/task/BookToEsTask.java index a456e3855a5313ae494b96299889eaf61b643cee..e1008f580df2d82c008086ef4b0165ea510a243b 100644 --- a/src/main/java/io/github/xxyopen/novel/core/task/BookToEsTask.java +++ b/src/main/java/io/github/xxyopen/novel/core/task/BookToEsTask.java @@ -6,6 +6,8 @@ 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; @@ -15,7 +17,6 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; @@ -26,7 +27,7 @@ import java.util.List; * @author xiongxiaoyang * @date 2022/5/23 */ -@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true") +@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enabled", havingValue = "true") @Component @RequiredArgsConstructor @Slf4j @@ -40,49 +41,54 @@ public class BookToEsTask { * 每月凌晨做一次全量数据同步 */ @SneakyThrows - @Scheduled(cron = "0 0 0 1 * ?") - public void saveToEs() { + @XxlJob("saveToEsJobHandler") + public ReturnT saveToEs() { - QueryWrapper queryWrapper = new QueryWrapper<>(); - List bookInfos; - long maxId = 0; - for(;;) { - queryWrapper.clear(); - queryWrapper + try { + QueryWrapper queryWrapper = new QueryWrapper<>(); + List 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(); + bookInfos = bookInfoMapper.selectList(queryWrapper); + if (bookInfos.isEmpty()) { + break; + } + BulkRequest.Builder br = new BulkRequest.Builder(); - for (BookInfo book : bookInfos) { - br.operations(op -> op + for (BookInfo book : bookInfos) { + br.operations(op -> op .index(idx -> idx - .index(EsConsts.BookIndex.INDEX_NAME) - .id(book.getId().toString()) - .document(EsBookDto.build(book)) + .index(EsConsts.BookIndex.INDEX_NAME) + .id(book.getId().toString()) + .document(EsBookDto.build(book)) ) - ).timeout(Time.of(t -> t.time("10s"))); - maxId = book.getId(); - } + ).timeout(Time.of(t -> t.time("10s"))); + maxId = book.getId(); + } - BulkResponse result = elasticsearchClient.bulk(br.build()); + 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()); + // 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; } - } } diff --git a/src/main/java/io/github/xxyopen/novel/core/util/JwtUtils.java b/src/main/java/io/github/xxyopen/novel/core/util/JwtUtils.java index 723d889c54424dcf431aee49600b2f92b6b1fe82..470eef7a9fdcc819751029d08375728fecf37d98 100644 --- a/src/main/java/io/github/xxyopen/novel/core/util/JwtUtils.java +++ b/src/main/java/io/github/xxyopen/novel/core/util/JwtUtils.java @@ -5,14 +5,13 @@ import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; -import java.nio.charset.StandardCharsets; -import java.util.Objects; - /** * JWT 工具类 * @@ -37,21 +36,23 @@ public class JwtUtils { /** * 根据用户ID生成JWT - * @param uid 用户ID + * + * @param uid 用户ID * @param systemKey 系统标识 * @return JWT */ public String generateToken(Long uid, String systemKey) { return Jwts.builder() - .setHeaderParam(HEADER_SYSTEM_KEY, systemKey) - .setSubject(uid.toString()) - .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) - .compact(); + .setHeaderParam(HEADER_SYSTEM_KEY, systemKey) + .setSubject(uid.toString()) + .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) + .compact(); } /** * 解析JWT返回用户ID - * @param token JWT + * + * @param token JWT * @param systemKey 系统标识 * @return 用户ID */ @@ -59,9 +60,9 @@ public class JwtUtils { Jws claimsJws; try { claimsJws = Jwts.parserBuilder() - .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) - .build() - .parseClaimsJws(token); + .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(token); // OK, we can trust this JWT // 判断该 JWT 是否属于指定系统 if (Objects.equals(claimsJws.getHeader().get(HEADER_SYSTEM_KEY), systemKey)) { diff --git a/src/main/java/io/github/xxyopen/novel/core/wrapper/XssHttpServletRequestWrapper.java b/src/main/java/io/github/xxyopen/novel/core/wrapper/XssHttpServletRequestWrapper.java index c96d94b988e036950bebda201d2ee701be6d139b..f743cb88dcd84b822c1b371b24c38649654a05f8 100644 --- a/src/main/java/io/github/xxyopen/novel/core/wrapper/XssHttpServletRequestWrapper.java +++ b/src/main/java/io/github/xxyopen/novel/core/wrapper/XssHttpServletRequestWrapper.java @@ -2,7 +2,6 @@ package io.github.xxyopen.novel.core.wrapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; - import java.util.HashMap; import java.util.Map; @@ -14,7 +13,7 @@ import java.util.Map; */ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { - private static final Map REPLACE_RULE = new HashMap<>(); + private static final Map REPLACE_RULE = new HashMap<>(); static { REPLACE_RULE.put("<", "<"); @@ -34,7 +33,8 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { for (int i = 0; i < length; i++) { escapeValues[i] = values[i]; int index = i; - REPLACE_RULE.forEach((k, v)-> escapeValues[index] = escapeValues[index].replaceAll(k, v)); + REPLACE_RULE.forEach( + (k, v) -> escapeValues[index] = escapeValues[index].replaceAll(k, v)); } return escapeValues; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/AuthorRegisterReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/AuthorRegisterReqDto.java index 2517b11e05e49648d0c41f4248ebb7d40c2e2f26..a4547c3bcc080af9d8fe5f6a36d68a04b889c406 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/AuthorRegisterReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/AuthorRegisterReqDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.req; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; import lombok.Data; @@ -12,38 +13,44 @@ import lombok.Data; @Data public class AuthorRegisterReqDto { + @Schema(hidden = true) private Long userId; /** * 笔名 */ - @NotBlank(message="笔名不能为空!") + @Schema(description = "笔名", required = true) + @NotBlank(message = "笔名不能为空!") private String penName; /** * 手机号码 */ - @NotBlank(message="手机号不能为空!") - @Pattern(regexp="^1[3|4|5|6|7|8|9][0-9]{9}$",message="手机号格式不正确!") + @Schema(description = "手机号码", required = true) + @NotBlank(message = "手机号不能为空!") + @Pattern(regexp = "^1[3|4|5|6|7|8|9][0-9]{9}$", message = "手机号格式不正确!") private String telPhone; /** * QQ或微信账号 */ - @NotBlank(message="QQ或微信账号不能为空!") + @Schema(description = "QQ或微信账号", required = true) + @NotBlank(message = "QQ或微信账号不能为空!") private String chatAccount; /** * 电子邮箱 */ - @NotBlank(message="电子邮箱不能为空!") - @Email(message="邮箱格式不正确!") + @Schema(description = "电子邮箱", required = true) + @NotBlank(message = "电子邮箱不能为空!") + @Email(message = "邮箱格式不正确!") private String email; /** * 作品方向;0-男频 1-女频 */ - @NotNull(message="作品方向不能为空!") + @Schema(description = "作品方向;0-男频 1-女频", required = true) + @NotNull(message = "作品方向不能为空!") @Min(0) @Max(1) private Integer workDirection; diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/BookAddReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/BookAddReqDto.java index f4ae0fa57942b583287d818ea56de2afea0df374..f89f9eced4a893573993f39861c9249e46b46681 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/BookAddReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/BookAddReqDto.java @@ -1,5 +1,6 @@ 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; @@ -16,42 +17,49 @@ 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; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/BookSearchReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/BookSearchReqDto.java index 0c342487d01273f3bbae970316c19851e9730089..015427cd97628f495a22ae309f8eb0ac54d4e77b 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/BookSearchReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/BookSearchReqDto.java @@ -2,6 +2,7 @@ package io.github.xxyopen.novel.dto.req; import com.fasterxml.jackson.annotation.JsonFormat; import io.github.xxyopen.novel.core.common.req.PageReqDto; +import io.swagger.v3.oas.annotations.Parameter; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; @@ -19,36 +20,43 @@ public class BookSearchReqDto extends PageReqDto { /** * 搜索关键字 */ + @Parameter(description = "搜索关键字") private String keyword; /** * 作品方向 */ + @Parameter(description = "作品方向") private Integer workDirection; /** * 分类ID */ + @Parameter(description = "分类ID") private Integer categoryId; /** * 是否收费,1:收费,0:免费 */ + @Parameter(description = "是否收费,1:收费,0:免费") private Integer isVip; /** * 小说更新状态,0:连载中,1:已完结 */ + @Parameter(description = "小说更新状态,0:连载中,1:已完结") private Integer bookStatus; /** * 字数最小值 */ + @Parameter(description = "字数最小值") private Integer wordCountMin; /** * 字数最大值 */ + @Parameter(description = "字数最大值") private Integer wordCountMax; /** @@ -57,6 +65,7 @@ public class BookSearchReqDto extends PageReqDto { * 如果使用Post请求,@RequestBody接收请求体参数,默认解析日期格式为yyyy-MM-dd HH:mm:ss , * 如果需要接收其他格式的参数,则可以使用@JsonFormat注解 * */ + @Parameter(description = "最小更新时间") @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private Date updateTimeMin; @@ -64,5 +73,6 @@ public class BookSearchReqDto extends PageReqDto { /** * 排序字段 */ + @Parameter(description = "排序字段") private String sort; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/ChapterAddReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/ChapterAddReqDto.java index b7ec4ec8e60f7120c42df55bf785ffba3d902ee8..cc07f5c04ca6a12dce77708fa26760dd95e7c14d 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/ChapterAddReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/ChapterAddReqDto.java @@ -1,5 +1,6 @@ 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; @@ -17,18 +18,20 @@ public class ChapterAddReqDto { /** * 小说ID */ - @NotNull + @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; @@ -36,6 +39,7 @@ public class ChapterAddReqDto { /** * 是否收费;1-收费 0-免费 */ + @Schema(description = "是否收费;1-收费 0-免费", required = true) @NotNull private Integer isVip; diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/ChapterUpdateReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/ChapterUpdateReqDto.java new file mode 100644 index 0000000000000000000000000000000000000000..b13131001cd0a5f12a43c2c55b79969dba94606e --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/dto/req/ChapterUpdateReqDto.java @@ -0,0 +1,40 @@ +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; + +} diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/UserCommentReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/UserCommentReqDto.java index bf9ef3b10d19f8e97177d9c892f8dc114c8b3309..1ebdd2e9b57dd65ce580367b5bc8d1505c1dc1fe 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/UserCommentReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/UserCommentReqDto.java @@ -1,5 +1,6 @@ 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; @@ -15,9 +16,11 @@ public class UserCommentReqDto { private Long userId; + @Schema(description = "小说ID", required = true) @NotNull(message="小说ID不能为空!") private Long bookId; + @Schema(description = "评论内容", required = true) @NotBlank(message="评论不能为空!") @Length(min = 10,max = 512) private String commentContent; diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/UserInfoUptReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/UserInfoUptReqDto.java index 2366a71f765f5513f070bcdea979290b259368ff..ac0020cb3cd961ae55055f117eee7e699631c31b 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/UserInfoUptReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/UserInfoUptReqDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.req; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Pattern; @@ -16,12 +17,15 @@ public class UserInfoUptReqDto { private Long userId; + @Schema(description = "昵称") @Length(min = 2,max = 10) private String nickName; + @Schema(description = "头像地址") @Pattern(regexp="^/[^\s]{10,}\\.(png|PNG|jpg|JPG|jpeg|JPEG|gif|GIF|bpm|BPM)$") private String userPhoto; + @Schema(description = "性别") @Min(value = 0) @Max(value = 1) private Integer userSex; diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/UserLoginReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/UserLoginReqDto.java index 1c88be3d19e375a1126fd303b287e2a2b66f74f0..d0f7754ab10dea4fb64f530c543aa8df368fdb73 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/UserLoginReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/UserLoginReqDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.req; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Data; @@ -13,11 +14,13 @@ import lombok.Data; @Data public class UserLoginReqDto { - @NotBlank(message="手机号不能为空!") - @Pattern(regexp="^1[3|4|5|6|7|8|9][0-9]{9}$",message="手机号格式不正确!") + @Schema(description = "手机号", required = true, example = "18888888888") + @NotBlank(message = "手机号不能为空!") + @Pattern(regexp = "^1[3|4|5|6|7|8|9][0-9]{9}$", message = "手机号格式不正确!") private String username; - @NotBlank(message="密码不能为空!") + @Schema(description = "密码", required = true, example = "123456") + @NotBlank(message = "密码不能为空!") private String password; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/req/UserRegisterReqDto.java b/src/main/java/io/github/xxyopen/novel/dto/req/UserRegisterReqDto.java index d2793ba6d729dd4cc95465fd137dc1ca73514401..0fa7e155408bf28c1e1b90e1442c00dfd9080738 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/req/UserRegisterReqDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/req/UserRegisterReqDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.req; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Data; @@ -14,13 +15,16 @@ import org.hibernate.validator.constraints.Length; @Data public class UserRegisterReqDto { + @Schema(description = "手机号", required = true) @NotBlank(message="手机号不能为空!") @Pattern(regexp="^1[3|4|5|6|7|8|9][0-9]{9}$",message="手机号格式不正确!") private String username; + @Schema(description = "密码", required = true) @NotBlank(message="密码不能为空!") private String password; + @Schema(description = "验证码", required = true) @NotBlank(message="验证码不能为空!") @Pattern(regexp="^\\d{4}$",message="验证码格式不正确!") private String velCode; @@ -28,6 +32,7 @@ public class UserRegisterReqDto { /** * 请求会话标识,用来标识图形验证码属于哪个会话 * */ + @Schema(description = "sessionId", required = true) @NotBlank @Length(min = 32,max = 32) private String sessionId; diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/BookCategoryRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/BookCategoryRespDto.java index 410a4b9f158fbdb9ca0aed2de32f859bfc24b919..e9e9a495744bd453f092ce854b1ff0f26bb63d2a 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/BookCategoryRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/BookCategoryRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -16,11 +17,13 @@ public class BookCategoryRespDto { /** * 类别ID */ + @Schema(description = "类别ID") private Long id; /** * 类别名 */ + @Schema(description = "类别名") private String name; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/BookChapterAboutRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/BookChapterAboutRespDto.java index 50b87c8bbb54e576325f993436d5fd6c3e9cae15..bef91708e9f3fe9acd447a05fe3fe3e5bc6c73a1 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/BookChapterAboutRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/BookChapterAboutRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -18,11 +19,13 @@ public class BookChapterAboutRespDto { /** * 章节总数 */ + @Schema(description = "章节总数") private Long chapterTotal; /** * 内容概要(30字) */ + @Schema(description = " 内容概要(30字)") private String contentSummary; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/BookChapterRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/BookChapterRespDto.java index 8d4b5d8e4de406d0949a420143efa8f002e8c713..c25786d83d517c4d0d103c4b47a429968ead8f22 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/BookChapterRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/BookChapterRespDto.java @@ -1,6 +1,7 @@ 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; @@ -23,33 +24,45 @@ public class BookChapterRespDto implements Serializable { /** * 章节ID - * */ + */ + @Schema(description = "章节ID") private Long id; /** * 小说ID */ + @Schema(description = "小说ID") private Long bookId; /** * 章节号 */ + @Schema(description = "章节号") private Integer chapterNum; /** * 章节名 */ + @Schema(description = "章节名") private String chapterName; /** * 章节字数 */ + @Schema(description = "章节字数") private Integer chapterWordCount; /** * 章节更新时间 */ - @JsonFormat(pattern = "yyyy/MM/dd HH:dd") + @Schema(description = "章节更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime chapterUpdateTime; + /** + * 是否收费;1-收费 0-免费 + */ + @Schema(description = "是否收费;1-收费 0-免费") + private Integer isVip; + } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/BookCommentRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/BookCommentRespDto.java index a632fdc5e5858a633716280aa254027f0447f715..f32ce3fbe717c562428759ed7d383203d0ccf930 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/BookCommentRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/BookCommentRespDto.java @@ -3,6 +3,7 @@ package io.github.xxyopen.novel.dto.resp; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.github.xxyopen.novel.core.json.serializer.UsernameSerializer; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -18,25 +19,33 @@ import java.util.List; @Builder public class BookCommentRespDto { + @Schema(description = "评论总数") private Long commentTotal; + @Schema(description = "评论列表") private List comments; @Data @Builder public static class CommentInfo { + @Schema(description = "评论ID") private Long id; + @Schema(description = "评论内容") private String commentContent; + @Schema(description = "评论用户") @JsonSerialize(using = UsernameSerializer.class) private String commentUser; + @Schema(description = "评论用户ID") private Long commentUserId; + @Schema(description = "评论用户头像") private String commentUserPhoto; + @Schema(description = "评论时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime commentTime; diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/BookContentAboutRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/BookContentAboutRespDto.java index bb8c01c19f013f8231d4290c7cdf60754efba608..96477f34f9201bd21372a20c8ba31be60245e489 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/BookContentAboutRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/BookContentAboutRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -16,16 +17,19 @@ public class BookContentAboutRespDto { /** * 小说信息 */ + @Schema(description = "小说信息") private BookInfoRespDto bookInfo; /** * 章节信息 */ + @Schema(description = "章节信息") private BookChapterRespDto chapterInfo; /** * 章节内容 */ + @Schema(description = "章节内容") private String bookContent; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/BookInfoRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/BookInfoRespDto.java index 55ba8094b725c0639d2bb79b014864b9ec72ccc4..6483bb110ae4fe04e17dbce0dc606eac63b22bda 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/BookInfoRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/BookInfoRespDto.java @@ -1,7 +1,11 @@ package io.github.xxyopen.novel.dto.resp; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; +import java.time.LocalDateTime; + /** * 小说信息 响应DTO * @@ -17,77 +21,99 @@ public class BookInfoRespDto { /** * ID */ + @Schema(description = "小说ID") private Long id; /** * 类别ID */ + @Schema(description = "类别ID") private Long categoryId; /** * 类别名 */ + @Schema(description = "类别名") private String categoryName; /** * 小说封面地址 */ + @Schema(description = "小说封面地址") private String picUrl; /** * 小说名 */ + @Schema(description = "小说名") private String bookName; /** * 作家id */ + @Schema(description = "作家id") private Long authorId; /** * 作家名 */ + @Schema(description = "作家名") private String authorName; /** * 书籍描述 */ + @Schema(description = "书籍描述") private String bookDesc; /** * 书籍状态;0-连载中 1-已完结 */ + @Schema(description = "书籍状态;0-连载中 1-已完结") private Integer bookStatus; /** * 点击量 */ + @Schema(description = "点击量") private Long visitCount; /** * 总字数 */ + @Schema(description = "总字数") private Integer wordCount; /** * 评论数 */ + @Schema(description = "评论数") private Integer commentCount; /** * 首章节ID */ + @Schema(description = "首章节ID") private Long firstChapterId; /** * 最新章节ID */ + @Schema(description = "最新章节ID") private Long lastChapterId; /** * 最新章节名 */ + @Schema(description = "最新章节名") private String lastChapterName; + /** + * 最新章节更新时间 + */ + @Schema(description = "最新章节更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime updateTime; + } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/BookRankRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/BookRankRespDto.java index 27c5f3dc15a8313cef60f68df20e6609af444ac5..475b90308750a3f2060121f94c6e7a391b55315b 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/BookRankRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/BookRankRespDto.java @@ -1,6 +1,7 @@ package io.github.xxyopen.novel.dto.resp; import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serial; @@ -22,51 +23,61 @@ public class BookRankRespDto implements Serializable { /** * ID */ + @Schema(description = "小说ID") private Long id; /** * 类别ID */ + @Schema(description = "类别ID") private Long categoryId; /** * 类别名 */ + @Schema(description = "类别名") private String categoryName; /** * 小说封面地址 */ + @Schema(description = "小说封面地址") private String picUrl; /** * 小说名 */ + @Schema(description = "小说名") private String bookName; /** * 作家名 */ + @Schema(description = "作家名") private String authorName; /** * 书籍描述 */ + @Schema(description = "书籍描述") private String bookDesc; /** * 总字数 */ + @Schema(description = "总字数") private Integer wordCount; /** * 最新章节名 */ + @Schema(description = "最新章节名") private String lastChapterName; /** * 最新章节更新时间 */ + @Schema(description = "最新章节更新时间") @JsonFormat(pattern = "MM/dd HH:mm") private LocalDateTime lastChapterUpdateTime; diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/ChapterContentRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/ChapterContentRespDto.java new file mode 100644 index 0000000000000000000000000000000000000000..6caebea33516b4a21b00fa12e8e428e92231f4ce --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/ChapterContentRespDto.java @@ -0,0 +1,35 @@ +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; + +} diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/HomeBookRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/HomeBookRespDto.java index 8a98e0beb2066ad1567fe1996c0a33d9b8332c8a..1808e0de8c8171d68c65c1f87a5ef8f262de17a7 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/HomeBookRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/HomeBookRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** @@ -14,31 +15,37 @@ public class HomeBookRespDto { /** * 类型;0-轮播图 1-顶部栏 2-本周强推 3-热门推荐 4-精品推荐 */ + @Schema(description = "类型;0-轮播图 1-顶部栏 2-本周强推 3-热门推荐 4-精品推荐") private Integer type; /** * 推荐小说ID */ + @Schema(description = "小说ID") private Long bookId; /** * 小说封面地址 */ + @Schema(description = "小说封面地址") private String picUrl; /** * 小说名 */ + @Schema(description = "小说名") private String bookName; /** * 作家名 */ + @Schema(description = "作家名") private String authorName; /** * 书籍描述 */ + @Schema(description = "书籍描述") private String bookDesc; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/HomeFriendLinkRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/HomeFriendLinkRespDto.java index 8478b71790eec19a4bf932dbaa19ccf714a26e04..7143ddf2d9f7ca3df00e6ba418fb71cb8ea8950f 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/HomeFriendLinkRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/HomeFriendLinkRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serial; @@ -20,10 +21,12 @@ public class HomeFriendLinkRespDto implements Serializable { /** * 链接名 */ + @Schema(description = "链接名") private String linkName; /** * 链接url */ + @Schema(description = "链接url") private String linkUrl; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/ImgVerifyCodeRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/ImgVerifyCodeRespDto.java index 635bd2a8b04b07ce4fffb5c2cf90aca2440b187a..0c477b62d193cfb4de73510081b6f38d685c5a16 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/ImgVerifyCodeRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/ImgVerifyCodeRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -15,11 +16,13 @@ public class ImgVerifyCodeRespDto { /** * 当前会话ID,用于标识改图形验证码属于哪个会话 * */ + @Schema(description = "sessionId") private String sessionId; /** * Base64 编码的验证码图片 * */ + @Schema(description = "Base64 编码的验证码图片") private String img; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/NewsInfoRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/NewsInfoRespDto.java index 91d2e54fe4bce837237eb375ff52acd50cd5a5b5..635b30c7cf9154fabce0cf99307fb3f5a9a9ca27 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/NewsInfoRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/NewsInfoRespDto.java @@ -1,6 +1,7 @@ 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; @@ -19,37 +20,44 @@ public class NewsInfoRespDto { /** * ID */ + @Schema(description = "新闻ID") private Long id; /** * 类别ID */ + @Schema(description = "类别ID") private Long categoryId; /** * 类别名 */ + @Schema(description = "类别名") private String categoryName; /** * 新闻来源 */ + @Schema(description = "新闻来源") private String sourceName; /** * 新闻标题 */ + @Schema(description = "新闻标题") private String title; /** * 更新时间 */ + @Schema(description = "更新时间") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDateTime updateTime; /** * 新闻内容 * */ + @Schema(description = "新闻内容") private String content; diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/UserCommentRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/UserCommentRespDto.java new file mode 100644 index 0000000000000000000000000000000000000000..3e44524569b0d57d234c60fd17c5d004bc48024d --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/UserCommentRespDto.java @@ -0,0 +1,33 @@ +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; + +} diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/UserInfoRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/UserInfoRespDto.java index c6f5213b32326136b9081958097d5218486e3b2a..753a65a4e74cde731e5caeeeaa186401d2542d1e 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/UserInfoRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/UserInfoRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -16,15 +17,18 @@ public class UserInfoRespDto { /** * 昵称 * */ + @Schema(description = "昵称") private String nickName; /** * 用户头像 * */ + @Schema(description = "用户头像") private String userPhoto; /** * 用户性别 * */ + @Schema(description = "用户性别") private Integer userSex; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/UserLoginRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/UserLoginRespDto.java index ad05e240cf4a600cfa536d5462db816561ba2467..c0d6066a2e35567376c528cafdad3d47d8a3f270 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/UserLoginRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/UserLoginRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -12,9 +13,12 @@ import lombok.Data; @Builder public class UserLoginRespDto { + @Schema(description = "用户ID") private Long uid; + @Schema(description = "用户昵称") private String nickName; + @Schema(description = "用户token") private String token; } diff --git a/src/main/java/io/github/xxyopen/novel/dto/resp/UserRegisterRespDto.java b/src/main/java/io/github/xxyopen/novel/dto/resp/UserRegisterRespDto.java index e6cb65ca6d4074f4944f0e274e77674fbbe1d419..8edf12548f9e36fc7043e46600f33dadb60662cc 100644 --- a/src/main/java/io/github/xxyopen/novel/dto/resp/UserRegisterRespDto.java +++ b/src/main/java/io/github/xxyopen/novel/dto/resp/UserRegisterRespDto.java @@ -1,5 +1,6 @@ package io.github.xxyopen.novel.dto.resp; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -12,7 +13,9 @@ import lombok.Data; @Builder public class UserRegisterRespDto { + @Schema(description = "用户ID") private Long uid; + @Schema(description = "用户token") private String token; } diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/AuthorInfoCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/AuthorInfoCacheManager.java index c48ac8c16a1510be290b4581702c2c9fa94ef755..f9ed88fa38b198114cc6ed2c4f39f4a96cf5841a 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/AuthorInfoCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/AuthorInfoCacheManager.java @@ -6,13 +6,12 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.dao.entity.AuthorInfo; import io.github.xxyopen.novel.dao.mapper.AuthorInfoMapper; import io.github.xxyopen.novel.dto.AuthorInfoDto; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; -import java.util.Objects; - /** * 作家信息 缓存管理类 * @@ -28,26 +27,26 @@ public class AuthorInfoCacheManager { /** * 查询作家信息,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER - , value = CacheConsts.AUTHOR_INFO_CACHE_NAME, unless = "#result == null") + @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, + value = CacheConsts.AUTHOR_INFO_CACHE_NAME, unless = "#result == null") public AuthorInfoDto getAuthor(Long userId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper - .eq(DatabaseConsts.AuthorInfoTable.COLUMN_USER_ID, userId) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + .eq(DatabaseConsts.AuthorInfoTable.COLUMN_USER_ID, userId) + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); AuthorInfo authorInfo = authorInfoMapper.selectOne(queryWrapper); if (Objects.isNull(authorInfo)) { return null; } return AuthorInfoDto.builder() - .id(authorInfo.getId()) - .penName(authorInfo.getPenName()) - .status(authorInfo.getStatus()).build(); + .id(authorInfo.getId()) + .penName(authorInfo.getPenName()) + .status(authorInfo.getStatus()).build(); } - @CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER - , value = CacheConsts.AUTHOR_INFO_CACHE_NAME) - public void evictAuthorCache(){ + @CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, + value = CacheConsts.AUTHOR_INFO_CACHE_NAME) + public void evictAuthorCache() { // 调用此方法自动清除作家信息的缓存 } diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/BookCategoryCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/BookCategoryCacheManager.java index a592a4aaf47a33a4c3c6fdd4680079adf7ddaf99..077f216c6d08f67d0641ff8d262dc61006fd78e2 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/BookCategoryCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/BookCategoryCacheManager.java @@ -6,12 +6,11 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.dao.entity.BookCategory; import io.github.xxyopen.novel.dao.mapper.BookCategoryMapper; import io.github.xxyopen.novel.dto.resp.BookCategoryRespDto; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; -import java.util.List; - /** * 小说分类 缓存管理类 * @@ -27,16 +26,16 @@ public class BookCategoryCacheManager { /** * 根据作品方向查询小说分类列表,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.BOOK_CATEGORY_LIST_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.BOOK_CATEGORY_LIST_CACHE_NAME) public List listCategory(Integer workDirection) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.BookCategoryTable.COLUMN_WORK_DIRECTION, workDirection); return bookCategoryMapper.selectList(queryWrapper).stream().map(v -> - BookCategoryRespDto.builder() - .id(v.getId()) - .name(v.getName()) - .build()).toList(); + BookCategoryRespDto.builder() + .id(v.getId()) + .name(v.getName()) + .build()).toList(); } } diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/BookChapterCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/BookChapterCacheManager.java index fe45fbe32181235a30852d3306f91658b44be339..237137c18a7357499fc71d12f19c2eadf877da8d 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/BookChapterCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/BookChapterCacheManager.java @@ -5,6 +5,7 @@ import io.github.xxyopen.novel.dao.entity.BookChapter; import io.github.xxyopen.novel.dao.mapper.BookChapterMapper; import io.github.xxyopen.novel.dto.resp.BookChapterRespDto; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; @@ -23,19 +24,25 @@ public class BookChapterCacheManager { /** * 查询小说章节信息,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.BOOK_CHAPTER_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.BOOK_CHAPTER_CACHE_NAME) public BookChapterRespDto getChapter(Long chapterId) { BookChapter bookChapter = bookChapterMapper.selectById(chapterId); return BookChapterRespDto.builder() - .id(chapterId) - .bookId(bookChapter.getBookId()) - .chapterNum(bookChapter.getChapterNum()) - .chapterName(bookChapter.getChapterName()) - .chapterWordCount(bookChapter.getWordCount()) - .chapterUpdateTime(bookChapter.getUpdateTime()) - .build(); + .id(chapterId) + .bookId(bookChapter.getBookId()) + .chapterNum(bookChapter.getChapterNum()) + .chapterName(bookChapter.getChapterName()) + .chapterWordCount(bookChapter.getWordCount()) + .chapterUpdateTime(bookChapter.getUpdateTime()) + .isVip(bookChapter.getIsVip()) + .build(); } + @CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.BOOK_CHAPTER_CACHE_NAME) + public void evictBookChapterCache(Long chapterId) { + // 调用此方法自动清除小说章节信息的缓存 + } } diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/BookContentCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/BookContentCacheManager.java index ea21719c349ec651d4d15841affca727977d2a5d..de41fb3ab175b012dfa9052b276218642f46893d 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/BookContentCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/BookContentCacheManager.java @@ -6,6 +6,7 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.dao.entity.BookContent; import io.github.xxyopen.novel.dao.mapper.BookContentMapper; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; @@ -24,15 +25,21 @@ public class BookContentCacheManager { /** * 查询小说内容,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER - , value = CacheConsts.BOOK_CONTENT_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, + value = CacheConsts.BOOK_CONTENT_CACHE_NAME) public String getBookContent(Long chapterId) { QueryWrapper contentQueryWrapper = new QueryWrapper<>(); contentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); BookContent bookContent = bookContentMapper.selectOne(contentQueryWrapper); return bookContent.getContent(); } + @CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, + value = CacheConsts.BOOK_CONTENT_CACHE_NAME) + public void evictBookContentCache(Long chapterId) { + // 调用此方法自动清除小说内容信息的缓存 + } + } diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/BookInfoCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/BookInfoCacheManager.java index ba509c566ec7cf8568730ba0e0aa59f7a7cb929b..94a7db190b511083fbb9d25b5f80fd2eec8c2a6e 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/BookInfoCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/BookInfoCacheManager.java @@ -9,6 +9,7 @@ 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.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; @@ -32,58 +33,63 @@ public class BookInfoCacheManager { /** * 从缓存中查询小说信息(先判断缓存中是否已存在,存在则直接从缓存中取,否则执行方法体中的逻辑后缓存结果) */ - @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.BOOK_INFO_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.BOOK_INFO_CACHE_NAME) public BookInfoRespDto getBookInfo(Long id) { return cachePutBookInfo(id); } /** * 缓存小说信息(不管缓存中是否存在都执行方法体中的逻辑,然后缓存起来) - * */ - @CachePut(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.BOOK_INFO_CACHE_NAME) + */ + @CachePut(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.BOOK_INFO_CACHE_NAME) public BookInfoRespDto cachePutBookInfo(Long id) { // 查询基础信息 BookInfo bookInfo = bookInfoMapper.selectById(id); // 查询首章ID QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper - .eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, id) - .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + .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(); + .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(); } - + @CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.BOOK_INFO_CACHE_NAME) + public void evictBookInfoCache(Long bookId) { + // 调用此方法自动清除小说信息的缓存 + } /** * 查询每个类别下最新更新的 500 个小说ID列表,并放入缓存中 1 个小时 */ - @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME) public List getLastUpdateIdList(Long categoryId) { QueryWrapper 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()); + .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0) + .orderByDesc(DatabaseConsts.BookTable.COLUMN_LAST_CHAPTER_UPDATE_TIME) + .last(DatabaseConsts.SqlEnum.LIMIT_500.getSql()); return bookInfoMapper.selectList(queryWrapper).stream().map(BookInfo::getId).toList(); } diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/BookRankCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/BookRankCacheManager.java index e616d52215c82277abef1bb5e2561784b21229e6..f0190791a294603bf0d79e1170af3383425b0704 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/BookRankCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/BookRankCacheManager.java @@ -6,12 +6,11 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.dao.entity.BookInfo; import io.github.xxyopen.novel.dao.mapper.BookInfoMapper; import io.github.xxyopen.novel.dto.resp.BookRankRespDto; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; -import java.util.List; - /** * 小说排行榜 缓存管理类 * @@ -27,40 +26,44 @@ public class BookRankCacheManager { /** * 查询小说点击榜列表,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER - , value = CacheConsts.BOOK_VISIT_RANK_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, + value = CacheConsts.BOOK_VISIT_RANK_CACHE_NAME) public List listVisitRankBooks() { QueryWrapper bookInfoQueryWrapper = new QueryWrapper<>(); bookInfoQueryWrapper.orderByDesc(DatabaseConsts.BookTable.COLUMN_VISIT_COUNT); - return getBookRankRespDtos(bookInfoQueryWrapper); + return listRankBooks(bookInfoQueryWrapper); } /** * 查询小说新书榜列表,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.BOOK_NEWEST_RANK_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.BOOK_NEWEST_RANK_CACHE_NAME) public List listNewestRankBooks() { QueryWrapper bookInfoQueryWrapper = new QueryWrapper<>(); bookInfoQueryWrapper - .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()); - return getBookRankRespDtos(bookInfoQueryWrapper); + .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0) + .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()); + return listRankBooks(bookInfoQueryWrapper); } /** * 查询小说更新榜列表,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.BOOK_UPDATE_RANK_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.BOOK_UPDATE_RANK_CACHE_NAME) public List listUpdateRankBooks() { QueryWrapper bookInfoQueryWrapper = new QueryWrapper<>(); bookInfoQueryWrapper - .orderByDesc(DatabaseConsts.CommonColumnEnum.UPDATE_TIME.getName()); - return getBookRankRespDtos(bookInfoQueryWrapper); + .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0) + .orderByDesc(DatabaseConsts.CommonColumnEnum.UPDATE_TIME.getName()); + return listRankBooks(bookInfoQueryWrapper); } - private List getBookRankRespDtos(QueryWrapper bookInfoQueryWrapper) { - bookInfoQueryWrapper.last(DatabaseConsts.SqlEnum.LIMIT_30.getSql()); + private List listRankBooks(QueryWrapper bookInfoQueryWrapper) { + bookInfoQueryWrapper + .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0) + .last(DatabaseConsts.SqlEnum.LIMIT_30.getSql()); return bookInfoMapper.selectList(bookInfoQueryWrapper).stream().map(v -> { BookRankRespDto respDto = new BookRankRespDto(); respDto.setId(v.getId()); diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/FriendLinkCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/FriendLinkCacheManager.java index b1876d8f06e37dabef78b0d6c71689f40600d44c..23f4488f96549bf01fb93074b52e990be3641e61 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/FriendLinkCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/FriendLinkCacheManager.java @@ -6,12 +6,11 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.dao.entity.HomeFriendLink; import io.github.xxyopen.novel.dao.mapper.HomeFriendLinkMapper; import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; -import java.util.List; - /** * 友情链接 缓存管理类 * @@ -27,8 +26,8 @@ public class FriendLinkCacheManager { /** * 友情链接列表查询,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER - , value = CacheConsts.HOME_FRIEND_LINK_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, + value = CacheConsts.HOME_FRIEND_LINK_CACHE_NAME) public List listFriendLinks() { // 从友情链接表中查询出友情链接列表 QueryWrapper queryWrapper = new QueryWrapper<>(); diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/HomeBookCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/HomeBookCacheManager.java index c94a95a7d8700e078dd00b9846c0977152a97232..7344ec77bd1112ed9210894561186231b12450d0 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/HomeBookCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/HomeBookCacheManager.java @@ -8,16 +8,15 @@ import io.github.xxyopen.novel.dao.entity.HomeBook; import io.github.xxyopen.novel.dao.mapper.BookInfoMapper; import io.github.xxyopen.novel.dao.mapper.HomeBookMapper; import io.github.xxyopen.novel.dto.resp.HomeBookRespDto; -import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; - import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; /** * 首页推荐小说 缓存管理类 @@ -36,8 +35,8 @@ public class HomeBookCacheManager { /** * 查询首页小说推荐,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.HOME_BOOK_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.HOME_BOOK_CACHE_NAME) public List listHomeBooks() { // 从首页小说推荐表中查询出需要推荐的小说 QueryWrapper queryWrapper = new QueryWrapper<>(); @@ -47,8 +46,8 @@ public class HomeBookCacheManager { // 获取推荐小说ID列表 if (!CollectionUtils.isEmpty(homeBooks)) { List bookIds = homeBooks.stream() - .map(HomeBook::getBookId) - .toList(); + .map(HomeBook::getBookId) + .toList(); // 根据小说ID列表查询相关的小说信息列表 QueryWrapper bookInfoQueryWrapper = new QueryWrapper<>(); @@ -56,9 +55,9 @@ public class HomeBookCacheManager { List bookInfos = bookInfoMapper.selectList(bookInfoQueryWrapper); // 组装 HomeBookRespDto 列表数据并返回 - if(!CollectionUtils.isEmpty(bookInfos)){ + if (!CollectionUtils.isEmpty(bookInfos)) { Map bookInfoMap = bookInfos.stream() - .collect(Collectors.toMap(BookInfo::getId, Function.identity())); + .collect(Collectors.toMap(BookInfo::getId, Function.identity())); return homeBooks.stream().map(v -> { BookInfo bookInfo = bookInfoMap.get(v.getBookId()); HomeBookRespDto bookRespDto = new HomeBookRespDto(); diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/NewsCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/NewsCacheManager.java index 6ffc5c82c7ab56749acb671d7f5cafd7a6444b59..6150dab3160da7b276b4f824cdbf30a45aeb5568 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/NewsCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/NewsCacheManager.java @@ -6,12 +6,11 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.dao.entity.NewsInfo; import io.github.xxyopen.novel.dao.mapper.NewsInfoMapper; import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; -import java.util.List; - /** * 新闻 缓存管理类 * @@ -27,21 +26,21 @@ public class NewsCacheManager { /** * 最新新闻列表查询,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER - , value = CacheConsts.LATEST_NEWS_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, + value = CacheConsts.LATEST_NEWS_CACHE_NAME) public List listLatestNews() { // 从新闻信息表中查询出最新发布的两条新闻 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()) - .last(DatabaseConsts.SqlEnum.LIMIT_2.getSql()); + .last(DatabaseConsts.SqlEnum.LIMIT_2.getSql()); return newsInfoMapper.selectList(queryWrapper).stream().map(v -> NewsInfoRespDto.builder() - .id(v.getId()) - .categoryId(v.getCategoryId()) - .categoryName(v.getCategoryName()) - .title(v.getTitle()) - .sourceName(v.getSourceName()) - .updateTime(v.getUpdateTime()) - .build()).toList(); + .id(v.getId()) + .categoryId(v.getCategoryId()) + .categoryName(v.getCategoryName()) + .title(v.getTitle()) + .sourceName(v.getSourceName()) + .updateTime(v.getUpdateTime()) + .build()).toList(); } } diff --git a/src/main/java/io/github/xxyopen/novel/manager/cache/UserInfoCacheManager.java b/src/main/java/io/github/xxyopen/novel/manager/cache/UserInfoCacheManager.java index 050db89893a1a8618750cff1b5550ca874831298..7a922781757bd1335a4b53a3a376a04f97054a88 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/cache/UserInfoCacheManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/cache/UserInfoCacheManager.java @@ -4,12 +4,11 @@ import io.github.xxyopen.novel.core.constant.CacheConsts; import io.github.xxyopen.novel.dao.entity.UserInfo; import io.github.xxyopen.novel.dao.mapper.UserInfoMapper; import io.github.xxyopen.novel.dto.UserInfoDto; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; -import java.util.Objects; - /** * 用户信息 缓存管理类 * @@ -25,16 +24,16 @@ public class UserInfoCacheManager { /** * 查询用户信息,并放入缓存中 */ - @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER - , value = CacheConsts.USER_INFO_CACHE_NAME) + @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, + value = CacheConsts.USER_INFO_CACHE_NAME) public UserInfoDto getUser(Long userId) { UserInfo userInfo = userInfoMapper.selectById(userId); - if(Objects.isNull(userInfo)){ + if (Objects.isNull(userInfo)) { return null; } return UserInfoDto.builder() - .id(userInfo.getId()) - .status(userInfo.getStatus()).build(); + .id(userInfo.getId()) + .status(userInfo.getStatus()).build(); } diff --git a/src/main/java/io/github/xxyopen/novel/manager/dao/UserDaoManager.java b/src/main/java/io/github/xxyopen/novel/manager/dao/UserDaoManager.java index 7020f18f343bf70c20d5bf44c6d819e358aabfdd..6ae41a854667537262e8b8b1f796a8011b2625f9 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/dao/UserDaoManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/dao/UserDaoManager.java @@ -4,13 +4,13 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.dao.entity.UserInfo; import io.github.xxyopen.novel.dao.mapper.UserInfoMapper; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import java.util.List; - /** * 用户模块 DAO管理类 + * * @author xiongxiaoyang * @date 2022/5/20 */ @@ -22,12 +22,13 @@ public class UserDaoManager { /** * 根据用户ID集合批量查询用户信息列表 + * * @param userIds 需要查询的用户ID集合 * @return 满足条件的用户信息列表 - * */ - public List listUsers(List userIds){ + */ + public List listUsers(List userIds) { QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.in(DatabaseConsts.CommonColumnEnum.ID.getName(),userIds); + queryWrapper.in(DatabaseConsts.CommonColumnEnum.ID.getName(), userIds); return userInfoMapper.selectList(queryWrapper); } diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMailSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMailSender.java new file mode 100644 index 0000000000000000000000000000000000000000..10d64e6276a4230320c5c985eb715fba8bd949c5 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMailSender.java @@ -0,0 +1,50 @@ +package io.github.xxyopen.novel.manager.message; + +import io.github.xxyopen.novel.core.config.MailProperties; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; + +/** + * 抽象的邮件消息发送者 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractMailSender extends AbstractMessageSender { + + private final MailProperties mailProperties; + + private final JavaMailSender mailSender; + + @Override + protected void sendMessage(Long toUserId, String messageTitle, String messageContent) { + // TODO 根据消息接收方的用户ID查询出消息接收方的邮件地址 + String toEmail = "xxyopen@foxmail.com"; + // 开始发送邮件 + log.info("发送 HTML 邮件开始:{},{},{}", toEmail, messageTitle, messageContent); + // 使用 MimeMessage,MIME 协议 + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper; + // MimeMessageHelper 帮助我们设置更丰富的内容 + try { + helper = new MimeMessageHelper(message, true); + helper.setFrom(new InternetAddress(mailProperties.username(), mailProperties.nickname(), "UTF-8")); + helper.setTo(toEmail); + helper.setSubject(messageTitle); + // 第二个参数 true 代表支持 html + helper.setText(messageContent, true); + mailSender.send(message); + log.info("发送 HTML 邮件 to {} 成功", toEmail); + } catch (Exception e) { + // 邮件发送失败不会重试 + log.error("发送 HTML 邮件 to {} 失败", toEmail, e); + } + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMessageSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMessageSender.java new file mode 100644 index 0000000000000000000000000000000000000000..35597793224bb2bacee7f16fea74f8479a434e3e --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractMessageSender.java @@ -0,0 +1,92 @@ +package io.github.xxyopen.novel.manager.message; + +/** + * 抽象的消息发送器 + *

+ * 遵循松耦合的设计原则,所有的属性都使用构造函数注入,与 Spring 框架解藕 + *

+ * 所有的消息发送器既可以注册到 Spring 容器中,作为 Spring 的一个组件使用,也可以直接通过 new 对象的方式使用 + *

+ * 每种类型的消息发送时机可能都不一样,不同类型和发送时机的消息格式可能也不一样,所以由各个子类去拓展消息的格式 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +public abstract class AbstractMessageSender implements MessageSender { + + private static final String PLACEHOLDER = "{}"; + + /** + * 定义消息发送的模版,子类不能修改此模版 + */ + @Override + public final void sendMessage(Long toUserId, Object... args) { + // 1.获取消息标题模版 + String titleTemplate = getTitleTemplate(); + // 2.获取消息内容模版 + String contentTemplate = getContentTemplate(); + // 3.解析消息模版,得到最终需要发送的消息标题 + String title = resolveTitle(titleTemplate, args); + // 4.解析消息内容,得到最终需要发送的消息内容 + String content = resolveContent(contentTemplate, args); + // 5.发送消息 + sendMessage(toUserId, title, content); + } + + /** + * 发送消息,具体发送到哪里由子类决定 + * + * @param toUserId 消息接收方的用户ID + * @param messageTitle 消息标题 + * @param messageContent 消息内容 + */ + protected abstract void sendMessage(Long toUserId, String messageTitle, String messageContent); + + /** + * 获取消息标题的模版,具体如何制定模版由子类决定 + * + * @return 消息标题 + */ + protected abstract String getTitleTemplate(); + + /** + * 获取消息内容的模版,具体如何制定模版由子类决定 + * + * @return 消息内容 + */ + protected abstract String getContentTemplate(); + + /** + * 通过给定的参数列表解析消息标题模版,默认固定标题,不需要解析,可以由子类来拓展它的功能 + * + * @param titleTemplate 消息标题模版 + * @param arguments 用来解析的参数列表 + * @return 解析后的消息标题 + */ + protected String resolveTitle(String titleTemplate, Object... arguments) { + return titleTemplate; + } + + /** + * 通过给定的参数列表解析消息内容模版,默认实现是使用参数列表来替换消息内容模版中的占位符,可以由子类来拓展它的功能 + *

+ * 子类可以根据第一个/前几个参数去数据库中查询动态内容,然后重组参数列表 + * + * @param contentTemplate 消息内容模版 + * @param args 用来解析的参数列表 + * @return 解析后的消息内容 + */ + protected String resolveContent(String contentTemplate, Object... args) { + if (args.length > 0) { + StringBuilder formattedContent = new StringBuilder(contentTemplate); + for (Object arg : args) { + int start = formattedContent.indexOf(PLACEHOLDER); + formattedContent.replace(start, start + PLACEHOLDER.length(), + String.valueOf(arg)); + } + return formattedContent.toString(); + } + return contentTemplate; + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/AbstractSysNoticeSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractSysNoticeSender.java new file mode 100644 index 0000000000000000000000000000000000000000..c457f259888325f46bba9ab31a859399cbfb3251 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/AbstractSysNoticeSender.java @@ -0,0 +1,26 @@ +package io.github.xxyopen.novel.manager.message; + +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 抽象的系统通知发送者 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +@Slf4j +public abstract class AbstractSysNoticeSender extends AbstractMessageSender { + + @Override + protected void sendMessage(Long toUserId, String messageTitle, String messageContent) { + // 生成消息的发送时间 + LocalDateTime messageDateTime = LocalDateTime.now(); + // TODO 在数据库系统通知表中插入一条记录 + log.info("系统通知发送成功,{},{},{},{}", toUserId, messageDateTime.format(DateTimeFormatter.ISO_DATE_TIME), + messageTitle, messageContent); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/MessageSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/MessageSender.java new file mode 100644 index 0000000000000000000000000000000000000000..ca109fa59bc3f8559d8fc5cdc0fc2a412be4b5b4 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/MessageSender.java @@ -0,0 +1,21 @@ +package io.github.xxyopen.novel.manager.message; + +/** + * 消息发送器接口,用来发送各种消息 + *

+ * 消息按类型分系统通知、邮件、短信、小程序通知等,按发送时机分注册成功消息、充值成功消息、活动通知消息、账户封禁消息、小说下架消息等 + * + * @author xiongxiaoyang + * @date 2023/3/25 + */ +public interface MessageSender { + + /** + * 发送消息,支持动态消息标题和动态消息内容 + * + * @param toUserId 消息接收方的用户ID + * @param args 用来动态生成消息标题和消息内容的参数列表 + */ + void sendMessage(Long toUserId, Object... args); + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/RegisterMailSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/RegisterMailSender.java new file mode 100644 index 0000000000000000000000000000000000000000..712ff858000d3e20e13bdb44dac41c8630a3342d --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/RegisterMailSender.java @@ -0,0 +1,60 @@ +package io.github.xxyopen.novel.manager.message; + +import io.github.xxyopen.novel.core.config.MailProperties; +import io.github.xxyopen.novel.core.constant.MessageSenderTypeConsts; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * 注册成功的邮件发送器 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +@Component(value = MessageSenderTypeConsts.REGISTER_MAIL_SENDER) +@EnableConfigurationProperties(MailProperties.class) +public class RegisterMailSender extends AbstractMailSender { + + public RegisterMailSender(MailProperties mailProperties, JavaMailSender mailSender) { + super(mailProperties, mailSender); + } + + @Override + protected String getTitleTemplate() { + return "欢迎来到小说精品屋"; + } + + @Override + protected String getContentTemplate() { + return """ +

+ 感谢你注册小说精品屋!你的账户现在处于活动状态。 +
+
    +
  • 你的账户电子邮件:{} +
  • 你的账户用户名:{} +
+ + + 如果你有任何问题,请通过 {} 与我们联系。 + """; + } + + @Override + protected String resolveContent(String content, Object... args) { + // TODO 去数据库/配置文件中查询网站配置 + String websiteLink = "https://www.xxyopen.com"; + String websiteEmail = "xxyopen@foxmail.com"; + return super.resolveContent(content, + Stream.of(args, new Object[]{websiteLink, websiteEmail}).flatMap(Arrays::stream).toArray()); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/message/SeckillSystemNoticeSender.java b/src/main/java/io/github/xxyopen/novel/manager/message/SeckillSystemNoticeSender.java new file mode 100644 index 0000000000000000000000000000000000000000..5a7fb4ae102b2e2cd09ca52404da341e7aae345b --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/manager/message/SeckillSystemNoticeSender.java @@ -0,0 +1,25 @@ +package io.github.xxyopen.novel.manager.message; + +import io.github.xxyopen.novel.core.constant.MessageSenderTypeConsts; +import org.springframework.stereotype.Component; + +/** + * 秒杀活动的系统通知发送器 + * + * @author xiongxiaoyang + * @date 2023/3/24 + */ +@Component(value = MessageSenderTypeConsts.SECKILL_SYS_NOTICE_SENDER) +public class SeckillSystemNoticeSender extends AbstractSysNoticeSender { + + @Override + protected String getTitleTemplate() { + return "秒杀即将开始"; + } + + @Override + protected String getContentTemplate() { + return "{}秒杀,{}即将开始,不要错过哦!点击 {} 前往。"; + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/manager/mq/AmqpMsgManager.java b/src/main/java/io/github/xxyopen/novel/manager/mq/AmqpMsgManager.java index ecdcb5ca7c7c4ab5a335a196449254e4ec31aff6..dd0ad1489e1d73fa165b31998bd1a278e527817a 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/mq/AmqpMsgManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/mq/AmqpMsgManager.java @@ -1,6 +1,5 @@ package io.github.xxyopen.novel.manager.mq; -import io.github.xxyopen.novel.core.common.constant.CommonConsts; import io.github.xxyopen.novel.core.constant.AmqpConsts; import lombok.RequiredArgsConstructor; import org.springframework.amqp.core.AmqpTemplate; @@ -9,8 +8,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; -import java.util.Objects; - /** * AMQP 消息管理类 * @@ -23,27 +20,29 @@ public class AmqpMsgManager { private final AmqpTemplate amqpTemplate; - @Value("${spring.amqp.enable}") - private String enableAmqp; + @Value("${spring.amqp.enabled:false}") + private boolean amqpEnabled; /** * 发送小说信息改变消息 */ public void sendBookChangeMsg(Long bookId) { - if (Objects.equals(enableAmqp, CommonConsts.TRUE)) { + if (amqpEnabled) { sendAmqpMessage(amqpTemplate, AmqpConsts.BookChangeMq.EXCHANGE_NAME, null, bookId); } } - private void sendAmqpMessage(AmqpTemplate amqpTemplate, String exchange, String routingKey, Object message) { + private void sendAmqpMessage(AmqpTemplate amqpTemplate, String exchange, String routingKey, + Object message) { // 如果在事务中则在事务执行完成后再发送,否则可以直接发送 if (TransactionSynchronizationManager.isActualTransactionActive()) { - TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { - @Override - public void afterCommit() { - amqpTemplate.convertAndSend(exchange, routingKey, message); - } - }); + TransactionSynchronizationManager.registerSynchronization( + new TransactionSynchronization() { + @Override + public void afterCommit() { + amqpTemplate.convertAndSend(exchange, routingKey, message); + } + }); return; } amqpTemplate.convertAndSend(exchange, routingKey, message); diff --git a/src/main/java/io/github/xxyopen/novel/manager/redis/VerifyCodeManager.java b/src/main/java/io/github/xxyopen/novel/manager/redis/VerifyCodeManager.java index d6578c2e74f688faa02c4093577b6ed13b746264..af873e6a1ec12f926fe22dd83be9d5dbaac83f89 100644 --- a/src/main/java/io/github/xxyopen/novel/manager/redis/VerifyCodeManager.java +++ b/src/main/java/io/github/xxyopen/novel/manager/redis/VerifyCodeManager.java @@ -2,15 +2,14 @@ package io.github.xxyopen.novel.manager.redis; import io.github.xxyopen.novel.core.common.util.ImgVerifyCodeUtils; import io.github.xxyopen.novel.core.constant.CacheConsts; +import java.io.IOException; +import java.time.Duration; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; -import java.io.IOException; -import java.time.Duration; -import java.util.Objects; - /** * 验证码 管理类 * @@ -25,27 +24,26 @@ public class VerifyCodeManager { private final StringRedisTemplate stringRedisTemplate; /** - * 生成图片验证码,并放入缓存中 + * 生成图形验证码,并放入 Redis 中 */ public String genImgVerifyCode(String sessionId) throws IOException { String verifyCode = ImgVerifyCodeUtils.getRandomVerifyCode(4); String img = ImgVerifyCodeUtils.genVerifyCodeImg(verifyCode); - stringRedisTemplate.opsForValue().set(CacheConsts.IMG_VERIFY_CODE_CACHE_KEY + sessionId - , verifyCode, Duration.ofMinutes(5)); + stringRedisTemplate.opsForValue().set(CacheConsts.IMG_VERIFY_CODE_CACHE_KEY + sessionId, + verifyCode, Duration.ofMinutes(5)); return img; } /** - * 校验图片验证码 + * 校验图形验证码 */ public boolean imgVerifyCodeOk(String sessionId, String verifyCode) { - return Objects.equals( - stringRedisTemplate.opsForValue().get(CacheConsts.IMG_VERIFY_CODE_CACHE_KEY + sessionId) - , verifyCode); + return Objects.equals(stringRedisTemplate.opsForValue() + .get(CacheConsts.IMG_VERIFY_CODE_CACHE_KEY + sessionId), verifyCode); } /** - * 删除验证码 + * 从 Redis 中删除验证码 */ public void removeImgVerifyCode(String sessionId) { stringRedisTemplate.delete(CacheConsts.IMG_VERIFY_CODE_CACHE_KEY + sessionId); diff --git a/src/main/java/io/github/xxyopen/novel/service/AuthorService.java b/src/main/java/io/github/xxyopen/novel/service/AuthorService.java index 672daa77bb431aa983304ba4ee970d4e24dbe2f9..0db8ff9776d403b3dc2c24bb9d8473223ab4e882 100644 --- a/src/main/java/io/github/xxyopen/novel/service/AuthorService.java +++ b/src/main/java/io/github/xxyopen/novel/service/AuthorService.java @@ -18,4 +18,12 @@ public interface AuthorService { * @return void */ RestResp register(AuthorRegisterReqDto dto); + + /** + * 查询作家状态 + * + * @param userId 用户ID + * @return 作家状态 + */ + RestResp getStatus(Long userId); } diff --git a/src/main/java/io/github/xxyopen/novel/service/BookService.java b/src/main/java/io/github/xxyopen/novel/service/BookService.java index 98e90594248f253f8de3f8a3078fc8da3a7b5947..1a725a85c422ec0456bf631022f51ed121ad2920 100644 --- a/src/main/java/io/github/xxyopen/novel/service/BookService.java +++ b/src/main/java/io/github/xxyopen/novel/service/BookService.java @@ -1,8 +1,11 @@ package io.github.xxyopen.novel.service; +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.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.req.UserCommentReqDto; import io.github.xxyopen.novel.dto.resp.*; @@ -128,32 +131,87 @@ public interface BookService { /** * 删除评论 - * @param userId 评论用户ID + * + * @param userId 评论用户ID * @param commentId 评论ID * @return void - * */ + */ RestResp deleteComment(Long userId, Long commentId); /** * 修改评论 - * @param userId 用户ID - * @param id 评论ID + * + * @param userId 用户ID + * @param id 评论ID * @param content 修改后的评论内容 * @return void - * */ + */ RestResp updateComment(Long userId, Long id, String content); /** * 小说信息保存 + * * @param dto 小说信息 * @return void - * */ + */ RestResp saveBook(BookAddReqDto dto); /** * 小说章节信息保存 + * * @param dto 章节信息 * @return void - * */ + */ RestResp saveBookChapter(ChapterAddReqDto dto); + + /** + * 查询作家发布小说列表 + * + * @param dto 分页请求参数 + * @return 小说分页列表数据 + */ + RestResp> listAuthorBooks(PageReqDto dto); + + /** + * 查询小说发布章节列表 + * + * @param bookId 小说ID + * @param dto 分页请求参数 + * @return 章节分页列表数据 + */ + RestResp> listBookChapters(Long bookId, PageReqDto dto); + + /** + * 分页查询评论 + * + * @param userId 会员ID + * @param pageReqDto 分页参数 + * @return 评论分页列表数据 + */ + RestResp> listComments(Long userId, PageReqDto pageReqDto); + + /** + * 小说章节删除 + * + * @param chapterId 章节ID + * @return void + */ + RestResp deleteBookChapter(Long chapterId); + + /** + * 小说章节查询 + * + * @param chapterId 章节ID + * @return 章节内容 + */ + RestResp getBookChapter(Long chapterId); + + /** + * 小说章节更新 + * + * @param chapterId 章节ID + * @param dto 更新内容 + * @return void + */ + RestResp updateBookChapter(Long chapterId, ChapterUpdateReqDto dto); } diff --git a/src/main/java/io/github/xxyopen/novel/service/impl/AuthorServiceImpl.java b/src/main/java/io/github/xxyopen/novel/service/impl/AuthorServiceImpl.java index 6c731bbc341ae0b94837d682d3f69b277e77f6b8..faa5bbf2d7445697520933f9c722d2f167a5b9b8 100644 --- a/src/main/java/io/github/xxyopen/novel/service/impl/AuthorServiceImpl.java +++ b/src/main/java/io/github/xxyopen/novel/service/impl/AuthorServiceImpl.java @@ -54,4 +54,10 @@ public class AuthorServiceImpl implements AuthorService { return RestResp.ok(); } + @Override + public RestResp getStatus(Long userId) { + AuthorInfoDto author = authorInfoCacheManager.getAuthor(userId); + return Objects.isNull(author) ? RestResp.ok(null) : RestResp.ok(author.getStatus()); + } + } diff --git a/src/main/java/io/github/xxyopen/novel/service/impl/BookServiceImpl.java b/src/main/java/io/github/xxyopen/novel/service/impl/BookServiceImpl.java index ff694bac086961dc951c83ce4d3c9fd6e56927c4..2054972f6c63e8fec98e324803e388d59c908162 100644 --- a/src/main/java/io/github/xxyopen/novel/service/impl/BookServiceImpl.java +++ b/src/main/java/io/github/xxyopen/novel/service/impl/BookServiceImpl.java @@ -1,8 +1,14 @@ package io.github.xxyopen.novel.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.github.xxyopen.novel.core.annotation.Key; +import io.github.xxyopen.novel.core.annotation.Lock; import io.github.xxyopen.novel.core.auth.UserHolder; import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum; +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.DatabaseConsts; import io.github.xxyopen.novel.dao.entity.*; @@ -13,6 +19,7 @@ import io.github.xxyopen.novel.dao.mapper.BookInfoMapper; import io.github.xxyopen.novel.dto.AuthorInfoDto; 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.req.UserCommentReqDto; import io.github.xxyopen.novel.dto.resp.*; import io.github.xxyopen.novel.manager.cache.*; @@ -23,6 +30,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -94,7 +102,8 @@ public class BookServiceImpl implements BookService { BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(bookId); // 查询最新章节信息 - BookChapterRespDto bookChapter = bookChapterCacheManager.getChapter(bookInfo.getLastChapterId()); + BookChapterRespDto bookChapter = bookChapterCacheManager.getChapter( + bookInfo.getLastChapterId()); // 查询章节内容 String content = bookContentCacheManager.getBookContent(bookInfo.getLastChapterId()); @@ -106,14 +115,15 @@ public class BookServiceImpl implements BookService { // 组装数据并返回 return RestResp.ok(BookChapterAboutRespDto.builder() - .chapterInfo(bookChapter) - .chapterTotal(chapterTotal) - .contentSummary(content.substring(0, 30)) - .build()); + .chapterInfo(bookChapter) + .chapterTotal(chapterTotal) + .contentSummary(content.substring(0, 30)) + .build()); } @Override - public RestResp> listRecBooks(Long bookId) throws NoSuchAlgorithmException { + public RestResp> listRecBooks(Long bookId) + throws NoSuchAlgorithmException { Long categoryId = bookInfoCacheManager.getBookInfo(bookId).getCategoryId(); List lastUpdateIdList = bookInfoCacheManager.getLastUpdateIdList(categoryId); List respDtoList = new ArrayList<>(); @@ -149,13 +159,13 @@ public class BookServiceImpl implements BookService { // 查询上一章信息并返回章节ID QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId) - .lt(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM, chapterNum) - .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + .lt(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM, chapterNum) + .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); return RestResp.ok( - Optional.ofNullable(bookChapterMapper.selectOne(queryWrapper)) - .map(BookChapter::getId) - .orElse(null) + Optional.ofNullable(bookChapterMapper.selectOne(queryWrapper)) + .map(BookChapter::getId) + .orElse(null) ); } @@ -169,13 +179,13 @@ public class BookServiceImpl implements BookService { // 查询下一章信息并返回章节ID QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId) - .gt(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM, chapterNum) - .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + .gt(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM, chapterNum) + .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); return RestResp.ok( - Optional.ofNullable(bookChapterMapper.selectOne(queryWrapper)) - .map(BookChapter::getId) - .orElse(null) + Optional.ofNullable(bookChapterMapper.selectOne(queryWrapper)) + .map(BookChapter::getId) + .orElse(null) ); } @@ -183,10 +193,12 @@ public class BookServiceImpl implements BookService { public RestResp> listChapters(Long bookId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId) - .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM); - return RestResp.ok(bookChapterMapper.selectList(queryWrapper).stream().map(v -> BookChapterRespDto.builder() + .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM); + return RestResp.ok(bookChapterMapper.selectList(queryWrapper).stream() + .map(v -> BookChapterRespDto.builder() .id(v.getId()) .chapterName(v.getChapterName()) + .isVip(v.getIsVip()) .build()).toList()); } @@ -195,13 +207,15 @@ public class BookServiceImpl implements BookService { return RestResp.ok(bookCategoryCacheManager.listCategory(workDirection)); } + @Lock(prefix = "userComment") @Override - public RestResp saveComment(UserCommentReqDto dto) { + public RestResp saveComment( + @Key(expr = "#{userId + '::' + bookId}") UserCommentReqDto dto) { // 校验用户是否已发表评论 QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID,dto.getUserId()) - .eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID,dto.getBookId()); - if(bookCommentMapper.selectCount(queryWrapper) > 0){ + queryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, dto.getUserId()) + .eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, dto.getBookId()); + if (bookCommentMapper.selectCount(queryWrapper) > 0) { // 用户已发表评论 return RestResp.fail(ErrorCodeEnum.USER_COMMENTED); } @@ -221,28 +235,30 @@ public class BookServiceImpl implements BookService { QueryWrapper commentCountQueryWrapper = new QueryWrapper<>(); commentCountQueryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, bookId); Long commentTotal = bookCommentMapper.selectCount(commentCountQueryWrapper); - BookCommentRespDto bookCommentRespDto = BookCommentRespDto.builder().commentTotal(commentTotal).build(); + BookCommentRespDto bookCommentRespDto = BookCommentRespDto.builder() + .commentTotal(commentTotal).build(); if (commentTotal > 0) { // 查询最新的评论列表 QueryWrapper commentQueryWrapper = new QueryWrapper<>(); commentQueryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, bookId) - .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()) - .last(DatabaseConsts.SqlEnum.LIMIT_5.getSql()); + .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()) + .last(DatabaseConsts.SqlEnum.LIMIT_5.getSql()); List bookComments = bookCommentMapper.selectList(commentQueryWrapper); // 查询评论用户信息,并设置需要返回的评论用户名 List userIds = bookComments.stream().map(BookComment::getUserId).toList(); List userInfos = userDaoManager.listUsers(userIds); - Map userInfoMap = userInfos.stream().collect(Collectors.toMap(UserInfo::getId, Function.identity())); + Map userInfoMap = userInfos.stream() + .collect(Collectors.toMap(UserInfo::getId, Function.identity())); List commentInfos = bookComments.stream() - .map(v -> BookCommentRespDto.CommentInfo.builder() - .id(v.getId()) - .commentUserId(v.getUserId()) - .commentUser(userInfoMap.get(v.getUserId()).getUsername()) - .commentUserPhoto(userInfoMap.get(v.getUserId()).getUserPhoto()) - .commentContent(v.getCommentContent()) - .commentTime(v.getCreateTime()).build()).toList(); + .map(v -> BookCommentRespDto.CommentInfo.builder() + .id(v.getId()) + .commentUserId(v.getUserId()) + .commentUser(userInfoMap.get(v.getUserId()).getUsername()) + .commentUserPhoto(userInfoMap.get(v.getUserId()).getUserPhoto()) + .commentContent(v.getCommentContent()) + .commentTime(v.getCreateTime()).build()).toList(); bookCommentRespDto.setComments(commentInfos); } else { bookCommentRespDto.setComments(Collections.emptyList()); @@ -254,8 +270,8 @@ public class BookServiceImpl implements BookService { public RestResp deleteComment(Long userId, Long commentId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.CommonColumnEnum.ID.getName(), commentId) - .eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID,userId); - bookCommentMapper.delete(queryWrapper); + .eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, userId); + bookCommentMapper.delete(queryWrapper); return RestResp.ok(); } @@ -263,15 +279,21 @@ public class BookServiceImpl implements BookService { public RestResp updateComment(Long userId, Long id, String content) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.CommonColumnEnum.ID.getName(), id) - .eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID,userId); + .eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, userId); BookComment bookComment = new BookComment(); bookComment.setCommentContent(content); - bookCommentMapper.update(bookComment,queryWrapper); + bookCommentMapper.update(bookComment, queryWrapper); return RestResp.ok(); } @Override public RestResp saveBook(BookAddReqDto dto) { + // 校验小说名是否已存在 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(DatabaseConsts.BookTable.COLUMN_BOOK_NAME, dto.getBookName()); + if (bookInfoMapper.selectCount(queryWrapper) > 0) { + return RestResp.fail(ErrorCodeEnum.AUTHOR_BOOK_NAME_EXIST); + } BookInfo bookInfo = new BookInfo(); // 设置作家信息 AuthorInfoDto author = authorInfoCacheManager.getAuthor(UserHolder.getUserId()); @@ -296,15 +318,20 @@ public class BookServiceImpl implements BookService { @Transactional(rollbackFor = Exception.class) @Override public RestResp saveBookChapter(ChapterAddReqDto dto) { + // 校验该作品是否属于当前作家 + BookInfo bookInfo = bookInfoMapper.selectById(dto.getBookId()); + if (!Objects.equals(bookInfo.getAuthorId(), UserHolder.getAuthorId())) { + return RestResp.fail(ErrorCodeEnum.USER_UN_AUTH); + } // 1) 保存章节相关信息到小说章节表 // a) 查询最新章节号 int chapterNum = 0; QueryWrapper chapterQueryWrapper = new QueryWrapper<>(); - chapterQueryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID,dto.getBookId()) - .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + chapterQueryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, dto.getBookId()) + .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); BookChapter bookChapter = bookChapterMapper.selectOne(chapterQueryWrapper); - if(Objects.nonNull(bookChapter)){ + if (Objects.nonNull(bookChapter)) { chapterNum = bookChapter.getChapterNum() + 1; } // b) 设置章节相关信息并保存 @@ -328,7 +355,6 @@ public class BookServiceImpl implements BookService { // 3) 更新小说表最新章节信息和小说总字数信息 // a) 更新小说表关于最新章节的信息 - BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(dto.getBookId()); BookInfo newBookInfo = new BookInfo(); newBookInfo.setId(dto.getBookId()); newBookInfo.setLastChapterId(newBookChapter.getId()); @@ -337,15 +363,190 @@ public class BookServiceImpl implements BookService { newBookInfo.setWordCount(bookInfo.getWordCount() + newBookChapter.getWordCount()); newBookChapter.setUpdateTime(LocalDateTime.now()); bookInfoMapper.updateById(newBookInfo); - // b) 刷新小说信息缓存 - bookInfoCacheManager.cachePutBookInfo(dto.getBookId()); + // b) 清除小说信息缓存 + bookInfoCacheManager.evictBookInfoCache(dto.getBookId()); // c) 发送小说信息更新的 MQ 消息 amqpMsgManager.sendBookChangeMsg(dto.getBookId()); return RestResp.ok(); } + @Override + public RestResp> listAuthorBooks(PageReqDto dto) { + IPage page = new Page<>(); + page.setCurrent(dto.getPageNum()); + page.setSize(dto.getPageSize()); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(DatabaseConsts.BookTable.AUTHOR_ID, UserHolder.getAuthorId()) + .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()); + IPage bookInfoPage = bookInfoMapper.selectPage(page, queryWrapper); + return RestResp.ok(PageRespDto.of(dto.getPageNum(), dto.getPageSize(), page.getTotal(), + bookInfoPage.getRecords().stream().map(v -> BookInfoRespDto.builder() + .id(v.getId()) + .bookName(v.getBookName()) + .picUrl(v.getPicUrl()) + .categoryName(v.getCategoryName()) + .wordCount(v.getWordCount()) + .visitCount(v.getVisitCount()) + .updateTime(v.getUpdateTime()) + .build()).toList())); + } + + @Override + public RestResp> listBookChapters(Long bookId, PageReqDto dto) { + IPage page = new Page<>(); + page.setCurrent(dto.getPageNum()); + page.setSize(dto.getPageSize()); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId) + .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM); + IPage bookChapterPage = bookChapterMapper.selectPage(page, queryWrapper); + return RestResp.ok(PageRespDto.of(dto.getPageNum(), dto.getPageSize(), page.getTotal(), + bookChapterPage.getRecords().stream().map(v -> BookChapterRespDto.builder() + .id(v.getId()) + .chapterName(v.getChapterName()) + .chapterUpdateTime(v.getUpdateTime()) + .isVip(v.getIsVip()) + .build()).toList())); + } + + @Override + public RestResp> listComments(Long userId, PageReqDto pageReqDto) { + IPage page = new Page<>(); + page.setCurrent(pageReqDto.getPageNum()); + page.setSize(pageReqDto.getPageSize()); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, userId) + .orderByDesc(DatabaseConsts.CommonColumnEnum.UPDATE_TIME.getName()); + IPage bookCommentPage = bookCommentMapper.selectPage(page, queryWrapper); + List comments = bookCommentPage.getRecords(); + if (!CollectionUtils.isEmpty(comments)) { + List bookIds = comments.stream().map(BookComment::getBookId).toList(); + QueryWrapper bookInfoQueryWrapper = new QueryWrapper<>(); + bookInfoQueryWrapper.in(DatabaseConsts.CommonColumnEnum.ID.getName(), bookIds); + Map bookInfoMap = bookInfoMapper.selectList(bookInfoQueryWrapper).stream() + .collect(Collectors.toMap(BookInfo::getId, Function.identity())); + return RestResp.ok(PageRespDto.of(pageReqDto.getPageNum(), pageReqDto.getPageSize(), page.getTotal(), + comments.stream().map(v -> UserCommentRespDto.builder() + .commentContent(v.getCommentContent()) + .commentBook(bookInfoMap.get(v.getBookId()).getBookName()) + .commentBookPic(bookInfoMap.get(v.getBookId()).getPicUrl()) + .commentTime(v.getCreateTime()) + .build()).toList())); + + } + return RestResp.ok(PageRespDto.of(pageReqDto.getPageNum(), pageReqDto.getPageSize(), page.getTotal(), + Collections.emptyList())); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public RestResp deleteBookChapter(Long chapterId) { + // 1.查询章节信息 + BookChapterRespDto chapter = bookChapterCacheManager.getChapter(chapterId); + // 2.查询小说信息 + BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(chapter.getBookId()); + // 3.删除章节信息 + bookChapterMapper.deleteById(chapterId); + // 4.删除章节内容 + QueryWrapper bookContentQueryWrapper = new QueryWrapper<>(); + bookContentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId); + bookContentMapper.delete(bookContentQueryWrapper); + // 5.更新小说信息 + BookInfo newBookInfo = new BookInfo(); + newBookInfo.setId(chapter.getBookId()); + newBookInfo.setUpdateTime(LocalDateTime.now()); + newBookInfo.setWordCount(bookInfo.getWordCount() - chapter.getChapterWordCount()); + if (Objects.equals(bookInfo.getLastChapterId(), chapterId)) { + // 设置最新章节信息 + QueryWrapper bookChapterQueryWrapper = new QueryWrapper<>(); + bookChapterQueryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, chapter.getBookId()) + .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + BookChapter bookChapter = bookChapterMapper.selectOne(bookChapterQueryWrapper); + Long lastChapterId = 0L; + String lastChapterName = ""; + LocalDateTime lastChapterUpdateTime = null; + if (Objects.nonNull(bookChapter)) { + lastChapterId = bookChapter.getId(); + lastChapterName = bookChapter.getChapterName(); + lastChapterUpdateTime = bookChapter.getUpdateTime(); + } + newBookInfo.setLastChapterId(lastChapterId); + newBookInfo.setLastChapterName(lastChapterName); + newBookInfo.setLastChapterUpdateTime(lastChapterUpdateTime); + } + bookInfoMapper.updateById(newBookInfo); + // 6.清理章节信息缓存 + bookChapterCacheManager.evictBookChapterCache(chapterId); + // 7.清理章节内容缓存 + bookContentCacheManager.evictBookContentCache(chapterId); + // 8.清理小说信息缓存 + bookInfoCacheManager.evictBookInfoCache(chapter.getBookId()); + // 9.发送小说信息更新的 MQ 消息 + amqpMsgManager.sendBookChangeMsg(chapter.getBookId()); + return RestResp.ok(); + } + + @Override + public RestResp getBookChapter(Long chapterId) { + BookChapterRespDto chapter = bookChapterCacheManager.getChapter(chapterId); + String bookContent = bookContentCacheManager.getBookContent(chapterId); + return RestResp.ok( + ChapterContentRespDto.builder() + .chapterName(chapter.getChapterName()) + .chapterContent(bookContent) + .isVip(chapter.getIsVip()) + .build()); + } + + @Transactional + @Override + public RestResp updateBookChapter(Long chapterId, ChapterUpdateReqDto dto) { + // 1.查询章节信息 + BookChapterRespDto chapter = bookChapterCacheManager.getChapter(chapterId); + // 2.查询小说信息 + BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(chapter.getBookId()); + // 3.更新章节信息 + BookChapter newChapter = new BookChapter(); + newChapter.setId(chapterId); + newChapter.setChapterName(dto.getChapterName()); + newChapter.setWordCount(dto.getChapterContent().length()); + newChapter.setIsVip(dto.getIsVip()); + newChapter.setUpdateTime(LocalDateTime.now()); + bookChapterMapper.updateById(newChapter); + // 4.更新章节内容 + BookContent newContent = new BookContent(); + newContent.setContent(dto.getChapterContent()); + newContent.setUpdateTime(LocalDateTime.now()); + QueryWrapper bookContentQueryWrapper = new QueryWrapper<>(); + bookContentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId); + bookContentMapper.update(newContent, bookContentQueryWrapper); + // 5.更新小说信息 + BookInfo newBookInfo = new BookInfo(); + newBookInfo.setId(chapter.getBookId()); + newBookInfo.setUpdateTime(LocalDateTime.now()); + newBookInfo.setWordCount( + bookInfo.getWordCount() - chapter.getChapterWordCount() + dto.getChapterContent().length()); + if (Objects.equals(bookInfo.getLastChapterId(), chapterId)) { + // 更新最新章节信息 + newBookInfo.setLastChapterName(dto.getChapterName()); + newBookInfo.setLastChapterUpdateTime(LocalDateTime.now()); + } + bookInfoMapper.updateById(newBookInfo); + // 6.清理章节信息缓存 + bookChapterCacheManager.evictBookChapterCache(chapterId); + // 7.清理章节内容缓存 + bookContentCacheManager.evictBookContentCache(chapterId); + // 8.清理小说信息缓存 + bookInfoCacheManager.evictBookInfoCache(chapter.getBookId()); + // 9.发送小说信息更新的 MQ 消息 + amqpMsgManager.sendBookChangeMsg(chapter.getBookId()); + return RestResp.ok(); + } + @Override public RestResp getBookContentAbout(Long chapterId) { + log.debug("userId:{}", UserHolder.getUserId()); // 查询章节信息 BookChapterRespDto bookChapter = bookChapterCacheManager.getChapter(chapterId); @@ -357,9 +558,9 @@ public class BookServiceImpl implements BookService { // 组装数据并返回 return RestResp.ok(BookContentAboutRespDto.builder() - .bookInfo(bookInfo) - .chapterInfo(bookChapter) - .bookContent(content) - .build()); + .bookInfo(bookInfo) + .chapterInfo(bookChapter) + .bookContent(content) + .build()); } } diff --git a/src/main/java/io/github/xxyopen/novel/service/impl/DbSearchServiceImpl.java b/src/main/java/io/github/xxyopen/novel/service/impl/DbSearchServiceImpl.java index 4418344e05054e035160928e3da09d1e01dce5d7..48477fe74212ad31663c22b49a7d2a097905c4b6 100644 --- a/src/main/java/io/github/xxyopen/novel/service/impl/DbSearchServiceImpl.java +++ b/src/main/java/io/github/xxyopen/novel/service/impl/DbSearchServiceImpl.java @@ -21,7 +21,7 @@ import java.util.List; * @author xiongxiaoyang * @date 2022/5/23 */ -@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "false") +@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enabled", havingValue = "false") @Service @RequiredArgsConstructor @Slf4j @@ -35,17 +35,18 @@ public class DbSearchServiceImpl implements SearchService { page.setCurrent(condition.getPageNum()); page.setSize(condition.getPageSize()); List bookInfos = bookInfoMapper.searchBooks(page, condition); - return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), page.getTotal() - , bookInfos.stream().map(v -> BookInfoRespDto.builder() - .id(v.getId()) - .bookName(v.getBookName()) - .categoryId(v.getCategoryId()) - .categoryName(v.getCategoryName()) - .authorId(v.getAuthorId()) - .authorName(v.getAuthorName()) - .wordCount(v.getWordCount()) - .lastChapterName(v.getLastChapterName()) - .build()).toList())); + return RestResp.ok( + PageRespDto.of(condition.getPageNum(), condition.getPageSize(), page.getTotal(), + bookInfos.stream().map(v -> BookInfoRespDto.builder() + .id(v.getId()) + .bookName(v.getBookName()) + .categoryId(v.getCategoryId()) + .categoryName(v.getCategoryName()) + .authorId(v.getAuthorId()) + .authorName(v.getAuthorName()) + .wordCount(v.getWordCount()) + .lastChapterName(v.getLastChapterName()) + .build()).toList())); } } diff --git a/src/main/java/io/github/xxyopen/novel/service/impl/EsSearchServiceImpl.java b/src/main/java/io/github/xxyopen/novel/service/impl/EsSearchServiceImpl.java index b03dcb8900257fd094f7c475bbd351ce0c3ef44a..b2b7d75b90a82bee0982ac5d63bbb761df522d0b 100644 --- a/src/main/java/io/github/xxyopen/novel/service/impl/EsSearchServiceImpl.java +++ b/src/main/java/io/github/xxyopen/novel/service/impl/EsSearchServiceImpl.java @@ -35,7 +35,7 @@ import java.util.Objects; * @author xiongxiaoyang * @date 2022/5/23 */ -@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true") +@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enabled", havingValue = "true") @Service @RequiredArgsConstructor @Slf4j @@ -49,74 +49,84 @@ public class EsSearchServiceImpl implements SearchService { SearchResponse response = esClient.search(s -> { - SearchRequest.Builder searchBuilder = s.index(EsConsts.BookIndex.INDEX_NAME); - // 构建检索条件 - buildSearchCondition(condition, searchBuilder); - // 排序 - if (!StringUtils.isBlank(condition.getSort())) { - searchBuilder.sort(o -> - o.field(f -> f.field(StringUtils - .underlineToCamel(condition.getSort().split(" ")[0])) - .order(SortOrder.Desc)) - ); - } - // 分页 - searchBuilder.from((condition.getPageNum() - 1) * condition.getPageSize()) - .size(condition.getPageSize()); - // 设置高亮显示 - searchBuilder.highlight(h -> h.fields(EsConsts.BookIndex.FIELD_BOOK_NAME - , t -> t.preTags("").postTags("")) - .fields(EsConsts.BookIndex.FIELD_AUTHOR_NAME - , t -> t.preTags("").postTags(""))); - - return searchBuilder; - }, - EsBookDto.class + SearchRequest.Builder searchBuilder = s.index(EsConsts.BookIndex.INDEX_NAME); + // 构建检索条件 + buildSearchCondition(condition, searchBuilder); + // 排序 + if (!StringUtils.isBlank(condition.getSort())) { + searchBuilder.sort(o -> o.field(f -> f + .field(StringUtils.underlineToCamel(condition.getSort().split(" ")[0])) + .order(SortOrder.Desc)) + ); + } + // 分页 + searchBuilder.from((condition.getPageNum() - 1) * condition.getPageSize()) + .size(condition.getPageSize()); + // 设置高亮显示 + searchBuilder.highlight(h -> h.fields(EsConsts.BookIndex.FIELD_BOOK_NAME, + t -> t.preTags("").postTags("")) + .fields(EsConsts.BookIndex.FIELD_AUTHOR_NAME, + t -> t.preTags("").postTags(""))); + + return searchBuilder; + }, + EsBookDto.class ); TotalHits total = response.hits().total(); List list = new ArrayList<>(); List> hits = response.hits().hits(); - for (Hit hit : hits) { + // 类型推断 var 非常适合 for 循环,JDK 10 引入,JDK 11 改进 + for (var hit : hits) { EsBookDto book = hit.source(); assert book != null; if (!CollectionUtils.isEmpty(hit.highlight().get(EsConsts.BookIndex.FIELD_BOOK_NAME))) { book.setBookName(hit.highlight().get(EsConsts.BookIndex.FIELD_BOOK_NAME).get(0)); } - if (!CollectionUtils.isEmpty(hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME))) { - book.setAuthorName(hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME).get(0)); + if (!CollectionUtils.isEmpty( + hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME))) { + book.setAuthorName( + hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME).get(0)); } list.add(BookInfoRespDto.builder() - .id(book.getId()) - .bookName(book.getBookName()) - .categoryId(book.getCategoryId()) - .categoryName(book.getCategoryName()) - .authorId(book.getAuthorId()) - .authorName(book.getAuthorName()) - .wordCount(book.getWordCount()) - .lastChapterName(book.getLastChapterName()) - .build()); + .id(book.getId()) + .bookName(book.getBookName()) + .categoryId(book.getCategoryId()) + .categoryName(book.getCategoryName()) + .authorId(book.getAuthorId()) + .authorName(book.getAuthorName()) + .wordCount(book.getWordCount()) + .lastChapterName(book.getLastChapterName()) + .build()); } assert total != null; - return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), total.value(), list)); + return RestResp.ok( + PageRespDto.of(condition.getPageNum(), condition.getPageSize(), total.value(), list)); } /** * 构建检索条件 */ - private void buildSearchCondition(BookSearchReqDto condition, SearchRequest.Builder searchBuilder) { + private void buildSearchCondition(BookSearchReqDto condition, + SearchRequest.Builder searchBuilder) { BoolQuery boolQuery = BoolQuery.of(b -> { + // 只查有字数的小说 + b.must(RangeQuery.of(m -> m + .field(EsConsts.BookIndex.FIELD_WORD_COUNT) + .gt(JsonData.of(0)) + )._toQuery()); + if (!StringUtils.isBlank(condition.getKeyword())) { // 关键词匹配 b.must((q -> q.multiMatch(t -> t - .fields(EsConsts.BookIndex.FIELD_BOOK_NAME + "^2" - , EsConsts.BookIndex.FIELD_AUTHOR_NAME + "^1.8" - , EsConsts.BookIndex.FIELD_BOOK_DESC + "^0.1") - .query(condition.getKeyword()) + .fields(EsConsts.BookIndex.FIELD_BOOK_NAME + "^2", + EsConsts.BookIndex.FIELD_AUTHOR_NAME + "^1.8", + EsConsts.BookIndex.FIELD_BOOK_DESC + "^0.1") + .query(condition.getKeyword()) ) )); } @@ -124,37 +134,37 @@ public class EsSearchServiceImpl implements SearchService { // 精确查询 if (Objects.nonNull(condition.getWorkDirection())) { b.must(TermQuery.of(m -> m - .field(EsConsts.BookIndex.FIELD_WORK_DIRECTION) - .value(condition.getWorkDirection()) + .field(EsConsts.BookIndex.FIELD_WORK_DIRECTION) + .value(condition.getWorkDirection()) )._toQuery()); } if (Objects.nonNull(condition.getCategoryId())) { b.must(TermQuery.of(m -> m - .field(EsConsts.BookIndex.FIELD_CATEGORY_ID) - .value(condition.getCategoryId()) + .field(EsConsts.BookIndex.FIELD_CATEGORY_ID) + .value(condition.getCategoryId()) )._toQuery()); } // 范围查询 if (Objects.nonNull(condition.getWordCountMin())) { b.must(RangeQuery.of(m -> m - .field(EsConsts.BookIndex.FIELD_WORD_COUNT) - .gte(JsonData.of(condition.getWordCountMin())) + .field(EsConsts.BookIndex.FIELD_WORD_COUNT) + .gte(JsonData.of(condition.getWordCountMin())) )._toQuery()); } if (Objects.nonNull(condition.getWordCountMax())) { b.must(RangeQuery.of(m -> m - .field(EsConsts.BookIndex.FIELD_WORD_COUNT) - .lt(JsonData.of(condition.getWordCountMax())) + .field(EsConsts.BookIndex.FIELD_WORD_COUNT) + .lt(JsonData.of(condition.getWordCountMax())) )._toQuery()); } if (Objects.nonNull(condition.getUpdateTimeMin())) { b.must(RangeQuery.of(m -> m - .field(EsConsts.BookIndex.FIELD_LAST_CHAPTER_UPDATE_TIME) - .gte(JsonData.of(condition.getUpdateTimeMin().getTime())) + .field(EsConsts.BookIndex.FIELD_LAST_CHAPTER_UPDATE_TIME) + .gte(JsonData.of(condition.getUpdateTimeMin().getTime())) )._toQuery()); } diff --git a/src/main/java/io/github/xxyopen/novel/service/impl/NewsServiceImpl.java b/src/main/java/io/github/xxyopen/novel/service/impl/NewsServiceImpl.java index c8d7431cb988644bb131bd6cd37952e07701e234..ebe1b2cb7cce43e0b430fc381dff613df461678c 100644 --- a/src/main/java/io/github/xxyopen/novel/service/impl/NewsServiceImpl.java +++ b/src/main/java/io/github/xxyopen/novel/service/impl/NewsServiceImpl.java @@ -10,11 +10,10 @@ import io.github.xxyopen.novel.dao.mapper.NewsInfoMapper; import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto; import io.github.xxyopen.novel.manager.cache.NewsCacheManager; import io.github.xxyopen.novel.service.NewsService; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; - /** * 新闻模块 服务实现类 * @@ -41,13 +40,13 @@ public class NewsServiceImpl implements NewsService { NewsInfo newsInfo = newsInfoMapper.selectById(id); QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.NewsContentTable.COLUMN_NEWS_ID, id) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); NewsContent newsContent = newsContentMapper.selectOne(queryWrapper); return RestResp.ok(NewsInfoRespDto.builder() - .title(newsInfo.getTitle()) - .sourceName(newsInfo.getSourceName()) - .updateTime(newsInfo.getUpdateTime()) - .content(newsContent.getContent()) - .build()); + .title(newsInfo.getTitle()) + .sourceName(newsInfo.getSourceName()) + .updateTime(newsInfo.getUpdateTime()) + .content(newsContent.getContent()) + .build()); } } diff --git a/src/main/java/io/github/xxyopen/novel/service/impl/ResourceServiceImpl.java b/src/main/java/io/github/xxyopen/novel/service/impl/ResourceServiceImpl.java index 4089d7880242448517fc4f9e4275c79e90bef9fd..eae937d6a3dd0f1c2f2dae4856dd6f93bf210662 100644 --- a/src/main/java/io/github/xxyopen/novel/service/impl/ResourceServiceImpl.java +++ b/src/main/java/io/github/xxyopen/novel/service/impl/ResourceServiceImpl.java @@ -8,20 +8,19 @@ import io.github.xxyopen.novel.core.constant.SystemConfigConsts; import io.github.xxyopen.novel.dto.resp.ImgVerifyCodeRespDto; import io.github.xxyopen.novel.manager.redis.VerifyCodeManager; import io.github.xxyopen.novel.service.ResourceService; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import javax.imageio.ImageIO; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Objects; +import javax.imageio.ImageIO; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; /** * 资源(图片/视频/文档)相关服务实现类 @@ -43,9 +42,9 @@ public class ResourceServiceImpl implements ResourceService { public RestResp getImgVerifyCode() throws IOException { String sessionId = IdWorker.get32UUID(); return RestResp.ok(ImgVerifyCodeRespDto.builder() - .sessionId(sessionId) - .img(verifyCodeManager.genImgVerifyCode(sessionId)) - .build()); + .sessionId(sessionId) + .img(verifyCodeManager.genImgVerifyCode(sessionId)) + .build()); } @SneakyThrows @@ -53,10 +52,10 @@ public class ResourceServiceImpl implements ResourceService { public RestResp uploadImage(MultipartFile file) { LocalDateTime now = LocalDateTime.now(); String savePath = - SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY - + now.format(DateTimeFormatter.ofPattern("yyyy")) + File.separator - + now.format(DateTimeFormatter.ofPattern("MM")) + File.separator - + now.format(DateTimeFormatter.ofPattern("dd")); + SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY + + now.format(DateTimeFormatter.ofPattern("yyyy")) + File.separator + + now.format(DateTimeFormatter.ofPattern("MM")) + File.separator + + now.format(DateTimeFormatter.ofPattern("dd")); String oriName = file.getOriginalFilename(); assert oriName != null; String saveFileName = IdWorker.get32UUID() + oriName.substring(oriName.lastIndexOf(".")); diff --git a/src/main/java/io/github/xxyopen/novel/service/impl/UserServiceImpl.java b/src/main/java/io/github/xxyopen/novel/service/impl/UserServiceImpl.java index 480bd83baf59fb72c1d5bf51d32aa2415f414d1c..3d3ff96ac6a81813ce587dea0ae84c31fee5dfb0 100644 --- a/src/main/java/io/github/xxyopen/novel/service/impl/UserServiceImpl.java +++ b/src/main/java/io/github/xxyopen/novel/service/impl/UserServiceImpl.java @@ -22,13 +22,12 @@ import io.github.xxyopen.novel.dto.resp.UserLoginRespDto; import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto; import io.github.xxyopen.novel.manager.redis.VerifyCodeManager; import io.github.xxyopen.novel.service.UserService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.util.DigestUtils; - import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; /** * 会员模块 服务实现类 @@ -61,7 +60,7 @@ public class UserServiceImpl implements UserService { // 校验手机号是否已注册 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.UserInfoTable.COLUMN_USERNAME, dto.getUsername()) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); if (userInfoMapper.selectCount(queryWrapper) > 0) { // 手机号已注册 throw new BusinessException(ErrorCodeEnum.USER_NAME_EXIST); @@ -69,7 +68,8 @@ public class UserServiceImpl implements UserService { // 注册成功,保存用户信息 UserInfo userInfo = new UserInfo(); - userInfo.setPassword(DigestUtils.md5DigestAsHex(dto.getPassword().getBytes(StandardCharsets.UTF_8))); + userInfo.setPassword( + DigestUtils.md5DigestAsHex(dto.getPassword().getBytes(StandardCharsets.UTF_8))); userInfo.setUsername(dto.getUsername()); userInfo.setNickName(dto.getUsername()); userInfo.setCreateTime(LocalDateTime.now()); @@ -82,10 +82,10 @@ public class UserServiceImpl implements UserService { // 生成JWT 并返回 return RestResp.ok( - UserRegisterRespDto.builder() - .token(jwtUtils.generateToken(userInfo.getId(), SystemConfigConsts.NOVEL_FRONT_KEY)) - .uid(userInfo.getId()) - .build() + UserRegisterRespDto.builder() + .token(jwtUtils.generateToken(userInfo.getId(), SystemConfigConsts.NOVEL_FRONT_KEY)) + .uid(userInfo.getId()) + .build() ); } @@ -95,7 +95,7 @@ public class UserServiceImpl implements UserService { // 查询用户信息 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.UserInfoTable.COLUMN_USERNAME, dto.getUsername()) - .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); + .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); UserInfo userInfo = userInfoMapper.selectOne(queryWrapper); if (Objects.isNull(userInfo)) { // 用户不存在 @@ -104,16 +104,16 @@ public class UserServiceImpl implements UserService { // 判断密码是否正确 if (!Objects.equals(userInfo.getPassword() - , DigestUtils.md5DigestAsHex(dto.getPassword().getBytes(StandardCharsets.UTF_8)))) { + , DigestUtils.md5DigestAsHex(dto.getPassword().getBytes(StandardCharsets.UTF_8)))) { // 密码错误 throw new BusinessException(ErrorCodeEnum.USER_PASSWORD_ERROR); } // 登录成功,生成JWT并返回 return RestResp.ok(UserLoginRespDto.builder() - .token(jwtUtils.generateToken(userInfo.getId(), SystemConfigConsts.NOVEL_FRONT_KEY)) - .uid(userInfo.getId()) - .nickName(userInfo.getNickName()).build()); + .token(jwtUtils.generateToken(userInfo.getId(), SystemConfigConsts.NOVEL_FRONT_KEY)) + .uid(userInfo.getId()) + .nickName(userInfo.getNickName()).build()); } @Override @@ -142,7 +142,7 @@ public class UserServiceImpl implements UserService { public RestResp deleteFeedback(Long userId, Long id) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.CommonColumnEnum.ID.getName(), id) - .eq(DatabaseConsts.UserFeedBackTable.COLUMN_USER_ID, userId); + .eq(DatabaseConsts.UserFeedBackTable.COLUMN_USER_ID, userId); userFeedbackMapper.delete(queryWrapper); return RestResp.ok(); } @@ -151,11 +151,11 @@ public class UserServiceImpl implements UserService { public RestResp getBookshelfStatus(Long userId, String bookId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.UserBookshelfTable.COLUMN_USER_ID, userId) - .eq(DatabaseConsts.UserBookshelfTable.COLUMN_BOOK_ID, bookId); + .eq(DatabaseConsts.UserBookshelfTable.COLUMN_BOOK_ID, bookId); return RestResp.ok( - userBookshelfMapper.selectCount(queryWrapper) > 0 - ? CommonConsts.YES - : CommonConsts.NO + userBookshelfMapper.selectCount(queryWrapper) > 0 + ? CommonConsts.YES + : CommonConsts.NO ); } @@ -163,9 +163,9 @@ public class UserServiceImpl implements UserService { public RestResp getUserInfo(Long userId) { UserInfo userInfo = userInfoMapper.selectById(userId); return RestResp.ok(UserInfoRespDto.builder() - .nickName(userInfo.getNickName()) - .userSex(userInfo.getUserSex()) - .userPhoto(userInfo.getUserPhoto()) - .build()); + .nickName(userInfo.getNickName()) + .userSex(userInfo.getUserSex()) + .userPhoto(userInfo.getUserPhoto()) + .build()); } } diff --git a/src/main/java/org/springframework/core/NestedIOException.java b/src/main/java/org/springframework/core/NestedIOException.java deleted file mode 100644 index 19d5f8804da26b914e2c01e096c64303e80c0771..0000000000000000000000000000000000000000 --- a/src/main/java/org/springframework/core/NestedIOException.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.springframework.core; - -import java.io.IOException; - -/** - * 兼容 mybatis-plus 3.5.1 - * mybatis-plus 的 MybatisSqlSessionFactoryBean 中使用到了这个异常 - * Spring 6 开始移除了该异常 - * - * @author xiongxiaoyang - * @date 2022/5/12 - */ -public class NestedIOException extends IOException { - -} diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..bdcd71d8abfb3dc422dcce71b83c478be7ff47d2 --- /dev/null +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,76 @@ +{ + "properties": [ + { + "name": "spring.elasticsearch.enabled", + "description": "Whether enable elasticsearch or not.", + "type": "java.lang.Boolean" + }, + { + "defaultValue": false, + "name": "spring.amqp.enabled", + "description": "Whether enable amqp or not.", + "type": "java.lang.Boolean" + }, + { + "name": "xxl.job.enabled", + "description": "Whether enable xxl-job or not.", + "type": "java.lang.Boolean" + }, + { + "name": "novel.jwt.secret", + "type": "java.lang.String", + "description": "JWT 密钥." + }, + { + "name": "novel.xss.enabled", + "type": "java.lang.Boolean", + "description": "是否开启 XSS 过滤." + }, + { + "name": "novel.xss.excludes", + "type": "java.util.List", + "description": "XSS 过滤排除链接." + }, + { + "name": "novel.file.upload.path", + "type": "java.lang.String", + "description": "上传文件目录." + }, + { + "name": "novel.cors.allow-origins", + "type": "java.util.List", + "description": "允许跨域的域名." + }, + { + "name": "xxl.job.admin.addresses", + "type": "java.lang.String", + "description": "调度中心部署根地址." + }, + { + "name": "xxl.job.executor.appname", + "type": "java.lang.String", + "description": "执行器 AppName." + }, + { + "name": "xxl.job.executor.logpath", + "type": "java.lang.String", + "description": "执行器运行日志文件存储磁盘路径." + }, + { + "name": "xxl.job.accessToken", + "type": "java.lang.String", + "description": "xxl-job accessToken." + }, + { + "name": "spring.elasticsearch.ssl.verification-mode", + "type": "java.lang.String", + "description": "设置 ssl 的认证模式,如果该配置项为 none ,说明不需要认证,信任所有的 ssl 证书." + }, + { + "defaultValue": true, + "name": "spring.shardingsphere.enabled", + "description": "Whether enable shardingsphere or not.", + "type": "java.lang.Boolean" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 41e220bd771e10af4b8cc62134dad70ff996c674..a91d118212affb8542de07b13548f1a9216c15d4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,50 +1,118 @@ +#--------------------------通用配置------------------------- spring: + application: + # 应用名 + name: novel profiles: + # 激活特定配置 active: dev - -# 将所有数字转为 String 类型返回,避免前端数据精度丢失的问题 jackson: generator: + # JSON 序列化时,将所有 Number 类型的属性都转为 String 类型返回,避免前端数据精度丢失的问题。 + # 由于 Javascript 标准规定所有数字处理都应使用 64 位 IEEE 754 浮点值完成, + # 结果是某些 64 位整数值无法准确表示(尾数只有 51 位宽) write-numbers-as-strings: true servlet: - # 上传文件最大大小 multipart: + # 上传文件最大大小 max-file-size: 5MB - + # 启用虚拟线程 + threads: + virtual: + enabled: true + # initialize the schema history table + flyway: + baseline-on-migrate: true server: + # 端口号 port: 8888 ---- +--- #---------------------数据库配置--------------------------- spring: datasource: url: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: test123456 - config: - activate: - on-profile: dev + # ShardingSphere-JDBC 配置 + # 配置是 ShardingSphere-JDBC 中唯一与应用开发者交互的模块, + # 通过它可以快速清晰的理解 ShardingSphere-JDBC 所提供的功能。 + shardingsphere: + # 是否开启分库分表 + enabled: false + props: + # 是否在日志中打印 SQL + sql-show: true + # 模式配置 + mode: + # 单机模式 + type: Standalone + # 元数据持久化 + repository: + # 数据库持久化 + type: JDBC + props: + # 元数据存储类型 + provider: H2 + jdbc_url: jdbc:h2:./.h2/shardingsphere ---- + # 数据源配置 + datasource: + names: ds_0 + ds_0: + type: com.zaxxer.hikari.HikariDataSource + driverClassName: com.mysql.cj.jdbc.Driver + jdbcUrl: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: test123456 + # 规则配置 + rules: + # 数据分片 + sharding: + tables: + # book_content 表 + book_content: + # 数据节点 + actual-data-nodes: ds_$->{0}.book_content$->{0..9} + # 分表策略 + table-strategy: + standard: + # 分片列名称 + sharding-column: chapter_id + # 分片算法名称 + sharding-algorithm-name: bookContentSharding + sharding-algorithms: + bookContentSharding: + # 行表达式分片算法,使用 Groovy 的表达式,提供对 SQL 语句中的 = 和 IN 的分片操作支持 + type: INLINE + props: + # 分片算法的行表达式 + algorithm-expression: book_content$->{chapter_id % 10} + +--- #---------------------中间件配置--------------------------- spring: - # Redis 配置 - redis: - host: 127.0.0.1 - port: 6379 - password: 123456 - config: - activate: - on-profile: dev + data: + # Redis 配置 + redis: + host: 127.0.0.1 + port: 6379 + password: test123456 + # Elasticsearch 配置 elasticsearch: - # 是否开启 elasticsearch 搜索引擎功能:true-开启 false-不开启 - enable: false + # 是否开启 Elasticsearch 搜索引擎功能:true-开启 false-不开启 + enabled: false uris: - https://my-deployment-ce7ca3.es.us-central1.gcp.cloud.es.io:9243 username: elastic password: qTjgYVKSuExX6tWAsDuvuvwl + # 设置 ssl 的认证模式,如果该配置项为 none ,说明不需要认证,信任所有的 ssl 证书。 + # ssl: + # verification-mode: none + + # Spring AMQP 配置 amqp: # 是否开启 Spring AMQP:true-开启 false-不开启 - enable: false + enabled: false # RabbitMQ 配置 rabbitmq: addresses: "amqp://guest:guest@47.106.243.172" @@ -58,13 +126,110 @@ spring: # 第一次和第二次重试之间的持续时间 initial-interval: "3s" ---- +# XXL-JOB 配置 +xxl: + job: + # 是否开启 XXL-JOB:true-开启 false-不开启 + enabled: false + admin: + ### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; + addresses: http://127.0.0.1:8080/xxl-job-admin + executor: + ### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 + appname: xxl-job-executor-novel + ### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; + logpath: logs/xxl-job/jobhandler + ### xxl-job, access token + accessToken: 123 + +--- #----------------------安全配置---------------------------- spring: - config: - activate: - on-profile: dev + # Spring Boot 应用管理和监控 + boot: + admin: + client: + # 是否开启 Spring Boot Admin 客户端 + enabled: false + # Spring Boot Admin 服务端注册地址 + url: http://localhost:8080 + # Spring Boot Admin 服务端认证用户名 + username: novel + # Spring Boot Admin 服务端认证密码 + password: novel + instance: + metadata: + # SBA Client + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} + security: + user: + name: ENDPOINT_ADMIN + password: ENDPOINT_ADMIN + roles: ENDPOINT_ADMIN + +# Actuator 端点管理 +management: + # 端点公开配置 + endpoints: + # 通过 HTTP 公开的 Web 端点 + web: + exposure: + # 公开所有的 Web 端点 + include: "*" + # 端点启用配置 + endpoint: + logfile: + # 启用返回日志文件内容的端点 + enabled: true + # 外部日志文件路径 + external-file: logs/novel.log + info: + env: + # 公开所有以 info. 开头的环境属性 + enabled: true + health: + rabbit: + # 关闭 rabbitmq 的健康检查 + enabled: false + elasticsearch: + # 关闭 elasticsearch 的健康检查 + enabled: false + mail: + # 关闭 mail 的健康检查 + enabled: false + +--- #--------------------接口文档配置--------------------------- +springdoc: + api-docs: + enabled: false -# 项目配置 +--- #----------------------邮箱配置----------------------------- +#邮箱服务器 +spring: + mail: + host: smtp.163.com + #发件人昵称 + nickname: xxyopen + #邮箱账户 + username: xxx@163.com + #邮箱第三方授权码 + password: xxx + #编码类型 + default-encoding: UTF-8 + port: 465 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: rue + socketFactory: + port: 465 + class: javax.net.ssl.SSLSocketFactory + fallback: false + +--- #---------------------自定义配置---------------------------- novel: # 跨域配置 cors: @@ -89,4 +254,37 @@ novel: path: /Users/xiongxiaoyang/upload +--- #------------------- dev 特定配置--------------------------- +spring: + config: + activate: + on-profile: dev +# 开启 SpringDoc 接口文档 +springdoc: + api-docs: + enabled: true +# /env 端点显示属性值 +management: + endpoint: + env: + show-values: when_authorized + +--- #------------------- test 特定配置-------------------------- +spring: + config: + activate: + on-profile: test + +--- #-------------------- prod 特定配置------------------------- +spring: + config: + activate: + on-profile: prod + data: + # Redis 配置 + redis: + host: 127.0.0.1 + port: 6379 + password: + diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 40569f64d5cbe110349935d14fb42de552fe6d7c..a7883d6e05cd02acca570273748052efa74d4745 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -53,26 +53,26 @@ + + - -