42 Commits

Author SHA1 Message Date
5b54e739d4 Merge github.com:201206030/novel 2022-08-04 23:58:33 +08:00
00acb47ec0 build: 3.3.0 发布 2022-08-04 23:57:51 +08:00
xxy
ada179053c Update README.md 2022-08-04 12:36:29 +08:00
xxy
1614e79106 Update README.md 2022-08-03 19:11:04 +08:00
xxy
8b43851955 Update README.md 2022-07-22 21:16:54 +08:00
xxy
8ba670cebb Update README.md 2022-07-21 22:03:36 +08:00
xxy
657c8a2286 Update README.md 2022-07-15 06:25:44 +08:00
cf4c6b77db Update README.md 2022-07-12 20:03:23 +08:00
6f6183af21 chore: 增加阿里云Maven中央仓库下载源 2022-07-09 20:32:09 +08:00
c3105c5b33 Update README.md 2022-07-08 14:33:23 +08:00
4f7a4f964a build: 依赖版本管理 2022-07-08 08:00:46 +08:00
fd9ce3cb49 docs: Java 样式文件修改 2022-07-08 07:59:24 +08:00
ae7267b605 Update README.md 2022-07-06 00:17:49 +08:00
9600f9c7e6 docs: 优化注释 2022-07-06 00:08:21 +08:00
eea6682870 docs: 优化注释 2022-07-05 23:58:09 +08:00
0890a3295d Merge github.com:201206030/novel 2022-07-02 09:58:19 +08:00
b0294a5b90 chore: git 忽略 .shardingsphere 目录 2022-07-02 09:55:47 +08:00
97b0e40534 Update README.md 2022-07-02 08:59:57 +08:00
5852cc6d26 style: 代码格式化 2022-07-01 06:30:11 +08:00
9b93d90270 style: 代码格式化 2022-06-30 21:34:54 +08:00
6d2fa74237 docs: 修改接口文档 2022-06-30 19:11:02 +08:00
76677ea8b8 Update README.md 2022-06-27 18:51:24 +08:00
e795d26aa5 docs: 接口文档修改 2022-06-27 18:33:44 +08:00
80480bc026 Update README.md 2022-06-27 12:41:13 +08:00
dec50ab0a0 feat: 集成 springdoc-openapi 自动生成 API 文档 2022-06-27 11:17:49 +08:00
c572650107 refactor: 使用类型推断 var 2022-06-23 20:58:38 +08:00
d9202c76fa Update README.md 2022-06-21 08:08:04 +08:00
fdc74bb7da Update README.md 2022-06-21 08:04:16 +08:00
5dceb7ac96 Update README.md 2022-06-21 07:51:29 +08:00
154b009c05 Update README.md 2022-06-21 07:48:12 +08:00
d7a9416c57 perf: LockAspect 优化 2022-06-21 07:33:25 +08:00
57793c4444 Update README.md 2022-06-21 06:44:49 +08:00
9bd95d3f28 feat: Spring AOP + Redisson 实现分布式锁 2022-06-20 13:20:00 +08:00
ac1628aa2a refactor: 使用 @ConfigurationProperties record 2022-06-19 09:53:58 +08:00
8e9f31a240 chore: 增加数据库脚本文件说明 2022-06-10 18:12:21 +08:00
37811bf173 chore: 修改 ShardingSphere-JDBC 的数据迁移脚本名,开启 ShardingSphere-JDBC 功能才需要执行 2022-06-10 17:33:58 +08:00
cfafc6450b chore: 增加 xxl_job 的脚本,开启 xxl-job 功能才需要执行 2022-06-10 17:31:22 +08:00
8199b3d4af docs: update README.md 2022-06-09 11:52:47 +08:00
fc0f3bee1d perf: 增加 Spring Boot Admin 的 SBA Client 配置 2022-06-07 20:57:06 +08:00
925da99d0b perf: Actuator Endpoints 保护 2022-06-07 18:17:08 +08:00
7012b7b8ea docs: update README.md 2022-06-07 08:24:40 +08:00
0a8808b418 build: 集成 Spring Boot Admin,实现应用管理和监控功能 2022-06-06 21:43:31 +08:00
101 changed files with 1560 additions and 706 deletions

1
.gitignore vendored
View File

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

109
README.md
View File

@ -1,9 +1,15 @@
[![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 )
<p align="center">
<a href='https://docs.oracle.com/en/java/javase/17'><img alt="Java 17" src="https://img.shields.io/badge/Java%2017-%234479A1.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAABNVBMVEUAAABkmP9ml/9mmf9mmf9lmv9nmf9mmf9mmf9nmP9mmf9mmf9mmv9mmf9mmf9mmf9mmf9mmP9llv9mmf9mmf9mmv9mmf9mmf9mmf9mmf//AABlmf9mmf9km/9mmf9mmf9lmf9mmf9mmf//AABmmf9mmf9mmf9lmv9mmf//AABmmf9mmP9mmf9mmf//AABgl/9mmf//AABmmP//AABmmf//AAD/AABmmf9mmv9mmf//AABnmf//AAD/AAD/AABmmf//AAD/AABlmf9mmf//AABmmf9mmv9mmf//AAD/AAD/AABmmf//AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AABmmf//AAD/AAD/AABsof9mmf//AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AAD/AABmmf//AAB37HanAAAAZXRSTlMAP4CLnb8dtpUP2plK78zEpyoKrGbVbxj334BjRzL7sVA168C8hXlEIwvJYJBXQQahWC8k5M6nW1Q6Myb2ya5yY0gT0rp9dWpSEerm35JtH5l7Xhjnzo2EdATz2dOfiDst8AW1KD5Fo/kAAAl3SURBVHja3Ny7ruIwEAbg/zloU9FEiqJQoCQiEsUh4k4ESNw56H//R1gHlsQk8TnbrcdfS8OIsT2escB/MoEjlnDEfAQ3ZMUDTlgwhRvIIZyQcAcneDzc4QKPDOACj3TjLPHICC7wSMIFR5JOFFxUXDjcQyoXyJeRbpzteyoLyOdR6UO8kMoN8u2pjCGfR+UM8b5IOlE0jlkKId2aigt33YQl+X2UPUu3DYTzqThwrZokLMVXCLfk0xbC9fiUSy8Ye3zpQbYRXyLhha9P0oXqpIpjANGqOGLZd1yfdGKlB3QjsVZ8SyDZiW+55GnCxmMlxE/sPl/8gpULzL52X7DZiDUfBv0g5dnu3+P8exyT9SDmt92t4E36axzDWU4Wc1jtEv0WR5BSOVneUlnffo5jsTpSiexe5MCOmhBN/jKmYvsix2TAWjFBw/D1se2LHBgmrI3v+LQ582UPy2UFa0s0zKd8Omaw3IiabTPIMV8G1nfpVtT08GEx418r2G5Lzbw7q3iwPq1woiaAblJ9ltrf2vpmLQ+huySC7rsD1o74MLrxJbb9LFfmrHndfUbmPqy3/oc4pgKuu196HHfoepLaD48pK8nG8HvAfvcxK8W1ddTLGfHMWFtDd81JihmCBub6KpXUZ+xHrIwNtVcuIbF6rA2hu7Ik5gFKwsqp684r5dXcwtho6E+piHnaFLCStm9ZgqbSO9PWizHfBNSKevUe9xs5JysQz5RZGUkpPZPPQALjtnyCAN+mPbZHWXO3Hd8WxkBiCQ/7A7712/WJpDcPG77dW8eIrNl0tdo3rV1L1gZcrYWHuXZhJGCVLG6G0W1EUlSVMjMce1sKS65hdxsIGaUl18AwZjvwRcxL2cwwL5jxwxrWW/Jp2siex426qeXDaGUSdS/3FUlZx6LPp0O/3WDRWf7OobTv/qYZPxwtH63XO1dkmCrKGR8iPHQnT0rdAfbLYipF2AxwSp39k1Bg1H2rDaRtXMCuu6Y6U3OEBMvul/xj1nKIkHY2rK8FKzFk8Kj4HatH0gCuukwl7eSS1RiqSvcZGkYSX/oXHd2fkCVp/5Nw6yjYj+IyqxS3z72ZoEbdn3bu/HVpMI4D+Gf37S63uXRebSoeKCKoGQl+UyMpouiEIIhP//+fULoOc1/LdFsuev0ssrfu+Tx7ru15HHnmevUpmyeO7x6OTN6HvWH2Xony5OA9Wh+zMmiPuHP/RaSxP4Eserp/2fcys7R7i+d7Jfh1Zv+PrUd7cxCZ6tIj7n1vMNnO8SPQ/cdZrFdRD+9mYSvK7716kIHZrBM8ewOn8myKNn0p78EV+vACTmKQFQW/q6yHkBZ9QvKabxXaVFWAizVzBWYQ8JU6fiMJkDi9Q1u4xdkexGJCEm9hpyrhV7kmJEm3NRd3mJUOMRFgj0fjVxNICkGqGFIbQ0hMNdkkNUrEkELWIFEr/IqAuBkdDr+hdUgaiSEO4uVRLn7DlSBOswnpQES/jaE5xIks4HddiIs+DiruhnR+2UwsiI/Txj02XKqvV+c8J+IWz8IRGwzNYr1f962GcB5Bl/PBgBnhd0wJjhpgqAix4fHAhiNLApzCcGa1ZrVjL/nKjYIH6vNTfj4S4pPDWyiiakrkukfsaVZ7RbtBUvzA5JiXYr2ARyk+NYO0g0A+h3Gy6GLtLYTSCxKazjmMgctRnZoBpzCTqL8hLy/l8CwFV9UoexJGOJGVWN8ecjy5+KXptt0y/pZi+XSQlz0H/pyOobIASVsMm/JkXFyvlgHF01qFYziN7gYNu9iTiZrOCn24QIAhFbLNEDE0hmyzMVSBbGttIk09YQIkQsJQAOmQIRkTDHUhDbWgA8mYpFixFnMGSUiGjl9B8joWIgMJwZALiZtp+AUPiWhhyIfk+bj1Es5jEHMDjpIxJEHyKAyRLfhDrR7lK4jos7/pCN01pIDBr95JHQ9O05+upXYZv3nZhNs4NO50WUgDg3sKucqgS86LVcJrCZHaphPVfIMacBYe4lmIqIYfqxCQjhIeVX6n1EeuaL1sq6pYx1+pSwdR3lK4pRYhNaSCMVB8aj+KrOIXZhXS1OpeGqKyai5gz1DbtbkppE0nLTyTqNnDw4rMIyITVsH0TXi1gH9mZDaaAkTYCmrFFvxFfS/Pq8pJEXzJlh24FUk338I1YEur7oBru4eJynUx55t0l+x5BhznwPUxFuxsWCOmwxnrCH3477//dmT4F3gVFv4BXeThHzB+iSJk39RERAYyz8Md0oDTOQRcHUPFkEtNT8jSkle0qiDSLbgyxE/DXXpVrBK604dDwrBUJLWbMn5TJq+syhF4m3duzucq2kDiqSAYMGIBo5TuEK6Jieejp3A9HLyEWYKrwb7EsyldHa4IKZ6XYnB1T2f9jo9/SKTHBlyjaXCDJ+PsGlyDxWzKQlSJ4jb4OyOOkv/+4LdPLBlrU/5FvWFlcqCO8BYFkeHzUwEi0p6OEJpLroBflP1GpzRc/DLwrCn3Ouv5kgwV5S+fP0OHgLiVpDpuuVKqbVQbQJxmqzbuMHkBUtXDBsSmqZVxh25C2tZ4AzFheQypMqROt2IL0nH/4jYjwsK4bi0av2lC6sj41r67+B0J6SJ2JwlsiEUH91hrFlIyLPJt/ELyYI8Q25jp3aAKSRPkgNsgRmPAFM5H4aFRZUlAMgxdtmkLv8o19IMeWYbzzRi8RZmhGz1PgLiwRIfU1M1PK9YlOFDNvYVLkBYeU29rQYdw4DyOTkzyy67mi2X8mX/LfvsFhV24zNsxV8ejwnW1NqfxwXbPw5AV4FBfWLAzvTYNt9TOG4Fkqtaxr8xRX0JE5UVUDbjY21p+cFPAhI3MFdE/viHfifFOLlJmInmUnEn1WLidt8wlssk0zJMblTEG7rZq1BZw1GzOhHWfgMQIOtGzA5r7kumcAGSHYOHXpiSDoRwBqTAcvdacdOwlRZuc2s7dWKK7qSuF8v4eIcakqUa+V6q1BDhBlXbxq1wesunnQx3tLMYQagcHUxSpBEnqCxAztmTzzAZ/Vlk7kLQW/1JbVmcxHD2Ri6T2soAR5tiBVDjFShnR9TWeXPeaHmvAyQxWn05sSgtXTaJErSG/hRS1xpT/41IKoxumsqtMTWJPSa72xsW8PV8tKUnj2mK9jEe9U/l8zYDUhY8vtIUXU244mqz+9WXFfosYN/hKro5/aNQ2u6sx0bq6afldoqArbc+Dtm/EUeREaGET3n/rbc+4gP+iPgNo04Ue6Gbq9gAAAABJRU5ErkJggg=="></a>
<a href='https://docs.spring.io/spring-boot/docs/3.0.0-SNAPSHOT/reference/html'><img alt="Spring Boot 3" src="https://img.shields.io/badge/Spring%20Boot%203-%23000000.svg?logo=springboot"></a>
<a href='https://staging-cn.vuejs.org'><img alt="Vue 3" src="https://img.shields.io/badge/Vue%203%20-%232b3847.svg?logo=vue.js"></a><br/>
<a href='https://github.com/201206030/novel'><img alt="Github stars" src="https://img.shields.io/github/stars/201206030/novel?logo=github"></a>
<a href='https://github.com/201206030/novel'><img alt="Github forks" src="https://img.shields.io/github/forks/201206030/novel?logo=github"></a>
<a href='https://gitee.com/novel_dev_team/novel'><img alt="Gitee stars" src="https://gitee.com/novel_dev_team/novel/badge/star.svg?theme=gitee"></a>
<a href='https://gitee.com/novel_dev_team/novel'><img alt="Gitee forks" src="https://gitee.com/novel_dev_team/novel/badge/fork.svg?theme=gitee"></a>
<a href="https://github.com/201206030/novel"><img src="https://visitor-badge.glitch.me/badge?page_id=201206030.novel" alt="visitors"></a>
</p>
[![Github stars](https://img.shields.io/github/stars/201206030/novel?logo=github)](https://github.com/201206030/novel)
[![Github forks](https://img.shields.io/github/forks/201206030/novel?logo=github)](https://github.com/201206030/novel)
[![Gitee star](https://gitee.com/novel_dev_team/novel/badge/star.svg?theme=gitee)](https://gitee.com/novel_dev_team/novel)
[![Gitee fork](https://gitee.com/novel_dev_team/novel/badge/fork.svg?theme=gitee)](https://gitee.com/novel_dev_team/novel)
## 项目简介 ## 项目简介
novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开发的前后端分离的**学习型**小说项目,配备详细的项目教程手把手教你**从零开始**开发上线一个生产级别的 Java 系统,由小说门户系统、作家后台管理系统、平台后台管理系统、爬虫管理系统等多个子系统构成。包括小说推荐、作品检索、小说排行榜、小说阅读、小说评论、充值订阅、新闻发布等功能。 novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开发的前后端分离的**学习型**小说项目,配备详细的项目教程手把手教你**从零开始**开发上线一个生产级别的 Java 系统,由小说门户系统、作家后台管理系统、平台后台管理系统、爬虫管理系统等多个子系统构成。包括小说推荐、作品检索、小说排行榜、小说阅读、小说评论、充值订阅、新闻发布等功能。
@ -12,7 +18,7 @@ novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开
- 后端项目(更新中):[GitHub](https://github.com/201206030/novel) [码云](https://gitee.com/novel_dev_team/novel) - 后端项目(更新中):[GitHub](https://github.com/201206030/novel) [码云](https://gitee.com/novel_dev_team/novel)
- 前端项目(更新中):[GitHub](https://github.com/201206030/novel-front-web) [码云](https://gitee.com/novel_dev_team/novel-front-web) - 前端项目(更新中):[GitHub](https://github.com/201206030/novel-front-web) [码云](https://gitee.com/novel_dev_team/novel-front-web)
- 线上应用版:[GitHub](https://github.com/201206030/novel-plus) [码云](https://gitee.com/novel_dev_team/novel-plus) [演示地址](http://47.106.243.172:8888/) - 线上应用版:[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-cloud) [码云](https://gitee.com/novel_dev_team/novel-cloud) - 微服务版:[GitHub](https://github.com/201206030/novel-cloud) [码云](https://gitee.com/novel_dev_team/novel-cloud)
## 开发环境 ## 开发环境
@ -31,35 +37,38 @@ novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开
## 后端技术选型 ## 后端技术选型
| 技术 | 版本 | 说明 | 官网 | 学习 | | 技术 | 版本 | 说明 | 官网 | 学习 |
|:-------------------------------|:--------------:|---------------------| --------------------------------------- |:----------------------------------------------------------------------------------------:| |---------------------|:--------------:|---------------------| --------------------------------------- |:---------------------------------------------------------------------------------------:|
| Spring Boot | 3.0.0-SNAPSHOT | 容器 + MVC 框架 | https://spring.io/projects/spring-boot | [进入](https://youdoc.github.io/course/novel/11.html) | | 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 | 3.5.9 | ORM 框架 | http://www.mybatis.org | [进入](https://mybatis.org/mybatis-3/zh/index.html) |
| MyBatis-Plus | 3.5.1 | MyBatis 增强工具 | https://baomidou.com/ | [进入](https://baomidou.com/pages/24112f/) | | MyBatis-Plus | 3.5.1 | MyBatis 增强工具 | https://baomidou.com/ | [进入](https://baomidou.com/pages/24112f/) |
| JJWT | 0.11.5 | JWT 登录支持 | https://github.com/jwtk/jjwt | - | | JJWT | 0.11.5 | JWT 登录支持 | https://github.com/jwtk/jjwt | - |
| Lombok | 1.18.24 | 简化对象封装工具 | https://github.com/projectlombok/lombok | [进入](https://projectlombok.org/features/all) | | Lombok | 1.18.24 | 简化对象封装工具 | https://github.com/projectlombok/lombok | [进入](https://projectlombok.org/features/all) |
| Caffeine | 3.1.0 | 本地缓存支持 | https://github.com/ben-manes/caffeine | [进入](https://github.com/ben-manes/caffeine/wiki/Home-zh-CN) | | Caffeine | 3.1.0 | 本地缓存支持 | https://github.com/ben-manes/caffeine | [进入](https://github.com/ben-manes/caffeine/wiki/Home-zh-CN) |
| Redis | 7.0 | 分布式缓存支持 | https://redis.io | [进入](https://redis.io/docs) | | Redis | 7.0 | 分布式缓存支持 | https://redis.io | [进入](https://redis.io/docs) |
| MySQL | 8.0 | 数据库服务 | https://www.mysql.com | [进入](https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html) | | Redisson | 3.17.4 | 分布式锁实现 | https://github.com/redisson/redisson | [进入](https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95) |
| ShardingSphere-JDBC | 5.1.1 | 数据库分库分表支持 | https://shardingsphere.apache.org | [进入](https://shardingsphere.apache.org/document/5.1.1/cn/overview) | | 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) | | ShardingSphere-JDBC | 5.1.1 | 数据库分库分表支持 | https://shardingsphere.apache.org | [进入](https://shardingsphere.apache.org/document/5.1.1/cn/overview) |
| RabbitMQ | 3.10.2 | 开源消息中间件 | https://www.rabbitmq.com | [进入](https://www.rabbitmq.com/tutorials/tutorial-one-java.html) | | Elasticsearch | 8.2.0 | 搜索引擎服务 | https://www.elastic.co | [进入](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) |
| XXL-JOB | 2.3.1 | 分布式任务调度平台 | https://www.xuxueli.com/xxl-job | [进入](https://www.xuxueli.com/xxl-job) | | RabbitMQ | 3.10.2 | 开源消息中间件 | https://www.rabbitmq.com | [进入](https://www.rabbitmq.com/tutorials/tutorial-one-java.html) |
| Sentinel | 1.8.4 | 流量控制组件 | https://github.com/alibaba/Sentinel | [进入](https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5) | | XXL-JOB | 2.3.1 | 分布式任务调度平台 | https://www.xuxueli.com/xxl-job | [进入](https://www.xuxueli.com/xxl-job) |
| Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 | https://undertow.io | [进入](https://undertow.io/documentation.html) | | Sentinel | 1.8.4 | 流量控制组件 | https://github.com/alibaba/Sentinel | [进入](https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5) |
| Docker | - | 应用容器引擎 | https://www.docker.com/ | - | | Springdoc-openapi | 2.0.0-M4-SNAPSHOT | Swagger 3 接口文档自动生成 | https://github.com/springdoc/springdoc-openapi | [进入](https://springdoc.org/) |
| Jenkins | - | 自动化部署工具 | https://github.com/jenkinsci/jenkins | - | | 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) |
| Sonarqube | - | 代码质量控制 | https://www.sonarqube.org/ | - | | 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.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/zh/index.html | [进入](https://router.vuejs.org/zh/guide/) | | 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/zh | [进入](https://axios-http.com/zh/docs/intro) | | 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/zh-CN/ | [进入](https://element-plus.org/zh-CN/guide/design.html) | | element-plus | 2.2.0 | 基于 Vue 3面向设计师和开发者的组件库 | https://element-plus.org | [进入](https://element-plus.org/zh-CN/guide/design.html) |
## 编码规范 ## 编码规范
@ -88,6 +97,8 @@ io
| | +- resp -- 接口响应工具及响应数据格式封装 | | +- resp -- 接口响应工具及响应数据格式封装
| | +- util -- 通用工具 | | +- util -- 通用工具
| | | |
| +- annotation -- 自定义注解类
| +- aspect -- Spring AOP 切面
| +- auth -- 用户认证授权相关 | +- auth -- 用户认证授权相关
| +- config -- 业务相关配置 | +- config -- 业务相关配置
| +- constant -- 业务相关常量 | +- constant -- 业务相关常量
@ -168,6 +179,10 @@ io
![img](https://oscimg.oschina.net/oscnet/up-f849960f4c1303fea77d26e64fc505a7180.png) ![img](https://oscimg.oschina.net/oscnet/up-f849960f4c1303fea77d26e64fc505a7180.png)
11. 接口文档
![img](https://youdoc.github.io/img/novel/SwaggerUI.png)
## 安装步骤 ## 安装步骤
@ -175,7 +190,7 @@ io
- 下载后端源码 - 下载后端源码
``` ```bash
git clone https://gitee.com/novel_dev_team/novel.git git clone https://gitee.com/novel_dev_team/novel.git
``` ```
@ -193,7 +208,7 @@ git clone https://gitee.com/novel_dev_team/novel.git
1. 修改`src/resources/application.yml`配置文件中的数据源配置 1. 修改`src/resources/application.yml`配置文件中的数据源配置
``` ```yaml
spring: spring:
datasource: datasource:
url: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai url: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
@ -201,9 +216,9 @@ git clone https://gitee.com/novel_dev_team/novel.git
password: test123456 password: test123456
``` ```
2. 修改`src/resources/application.yml`配置文件中的`redis`连接配置 2. 修改`src/resources/application.yml` 和 `src/resources/redisson.yml` 配置文件中的`redis`连接配置
``` ```yaml
spring: spring:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
@ -211,17 +226,25 @@ git clone https://gitee.com/novel_dev_team/novel.git
password: 123456 password: 123456
``` ```
3. 项目根目录下运行如下命令来启动后端服务(有安装 IDE 的可以导入源码到 IDE 中运行) ```yaml
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: 123456
``` ```
3. 根据前后端的实际部署情况,修改`application.yml`中的跨域配置(默认情况可忽略此步骤)
4. 项目根目录下运行如下命令来启动后端服务(有安装 IDE 的可以导入源码到 IDE 中运行)
```bash
mvn spring-boot:run mvn spring-boot:run
``` ```
5. 接口文档访问地址:`http://server:port/swagger-ui/index.html`
4. 根据前后端的实际部署情况,修改`application.yml`中的跨域配置(默认情况可忽略此步骤)
- 下载前端前台门户系统源码 - 下载前端前台门户系统源码
``` ```bash
git clone https://gitee.com/novel_dev_team/novel-front-web.git git clone https://gitee.com/novel_dev_team/novel-front-web.git
``` ```
@ -231,25 +254,25 @@ git clone https://gitee.com/novel_dev_team/novel-front-web.git
2. `yarn`安装 2. `yarn`安装
``` ```bash
npm install -g yarn npm install -g yarn
``` ```
3. 项目根目录下运行如下命令来安装项目依赖 3. 项目根目录下运行如下命令来安装项目依赖
``` ```bash
yarn install yarn install
``` ```
4. 项目根目录下运行如下命令启动 4. 项目根目录下运行如下命令启动
``` ```bash
yarn serve yarn serve
``` ```
5. 浏览器通过`http://localhost:1024`来访问 5. 浏览器通过`http://localhost:1024`来访问
## 项目教程 ## 项目教程
[手把手教你从零开始开发上线一个生产级别的小说系统](https://youdoc.github.io/course/novel/3.html) [手把手教你从零开始开发上线一个生产级别的小说系统](https://docs.xxyopen.com/course/novel/3.html)
## 公众号 ## 公众号
@ -261,7 +284,7 @@ git clone https://gitee.com/novel_dev_team/novel-front-web.git
- 回复「**笔记**」获取`Spring Boot 3 学习笔记` - 回复「**笔记**」获取`Spring Boot 3 学习笔记`
![xxyopen](https://youdoc.gitee.io/img/qrcode_for_gh.jpg) ![xxyopen](https://youdoc.github.io/img/qrcode_for_gh.jpg)
## 赞赏支持 ## 赞赏支持

6
doc/sql/README.md Normal file
View File

@ -0,0 +1,6 @@
1. 初始状态下MySQL 只需要执行 `novel.sql` 文件即可正常运行本系统
2. 代码更新后再执行以日期命名的增量 SQL 文件
3. 只有开启 XXL-JOB 的功能,才需要执行 `xxl-job.sql` 和以 xxl-job 开头日期结尾的增量 SQL 文件
4. 只有开启 ShardingSphere-JDBC 的功能,才需要执行 `shardingsphere-jdbc.sql` 和以 shardingsphere-jdbc 开头日期结尾的增量 SQL
文件

190
doc/sql/xxl-job.sql Normal file
View File

@ -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
);

3
doc/style/README.md Normal file
View File

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

View File

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

86
pom.xml
View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
@ -10,7 +11,7 @@
</parent> </parent>
<groupId>io.github.xxyopen</groupId> <groupId>io.github.xxyopen</groupId>
<artifactId>novel</artifactId> <artifactId>novel</artifactId>
<version>3.2.0</version> <version>3.3.0</version>
<name>novel</name> <name>novel</name>
<description>Spring Boot 3 + Vue 3 构建的前后端分离小说系统</description> <description>Spring Boot 3 + Vue 3 构建的前后端分离小说系统</description>
<properties> <properties>
@ -22,7 +23,11 @@
<xxl-job.version>2.3.1</xxl-job.version> <xxl-job.version>2.3.1</xxl-job.version>
<sentinel.version>1.8.4</sentinel.version> <sentinel.version>1.8.4</sentinel.version>
<shardingsphere-jdbc.version>5.1.1</shardingsphere-jdbc.version> <shardingsphere-jdbc.version>5.1.1</shardingsphere-jdbc.version>
<redisson.version>3.17.4</redisson.version>
<spring-boot-admin.version>3.0.0-M1</spring-boot-admin.version>
<springdoc-openapi.version>2.0.0-M4-SNAPSHOT</springdoc-openapi.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@ -145,6 +150,41 @@
<version>${shardingsphere-jdbc.version}</version> <version>${shardingsphere-jdbc.version}</version>
</dependency> </dependency>
<!-- Spring Boot 管理和监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Redisson 相关 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- Aop 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- OpenAPI 3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>
@ -184,6 +224,16 @@
</plugins> </plugins>
</build> </build>
<repositories> <repositories>
<repository>
<id>ali</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository> <repository>
<id>spring-milestones</id> <id>spring-milestones</id>
<name>Spring Milestones</name> <name>Spring Milestones</name>
@ -200,8 +250,40 @@
<enabled>false</enabled> <enabled>false</enabled>
</releases> </releases>
</repository> </repository>
<repository>
<id>sonatype-nexus-snapshots</id>
<name>Sonatype Nexus Snapshots</name>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>sonatype-nexus-snapshots-2</id>
<name>Sonatype Nexus Snapshots 2</name>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories> </repositories>
<pluginRepositories> <pluginRepositories>
<pluginRepository>
<id>ali</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository> <pluginRepository>
<id>spring-milestones</id> <id>spring-milestones</id>
<name>Spring Milestones</name> <name>Spring Milestones</name>

View File

@ -1,18 +1,29 @@
package io.github.xxyopen.novel; package io.github.xxyopen.novel;
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 java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import java.util.Map; @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")
@SpringBootApplication @SpringBootApplication
@MapperScan("io.github.xxyopen.novel.dao.mapper") @MapperScan("io.github.xxyopen.novel.dao.mapper")
@EnableCaching @EnableCaching
@ -20,21 +31,30 @@ import java.util.Map;
@Slf4j @Slf4j
public class NovelApplication { public class NovelApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(NovelApplication.class, args); SpringApplication.run(NovelApplication.class, args);
} }
@Bean @Bean
public CommandLineRunner commandLineRunner(ApplicationContext context){ public CommandLineRunner commandLineRunner(ApplicationContext context) {
return args -> { return args -> {
Map<String, CacheManager> beans = context.getBeansOfType(CacheManager.class); Map<String, CacheManager> beans = context.getBeansOfType(CacheManager.class);
log.info("加载了如下缓存管理器:"); log.info("加载了如下缓存管理器:");
beans.forEach((k,v)->{ beans.forEach((k, v) -> {
log.info("{}:{}",k,v.getClass().getName()); log.info("{}:{}", k, v.getClass().getName());
log.info("缓存:{}",v.getCacheNames()); log.info("缓存:{}", v.getCacheNames());
}); });
}; };
} }
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.requestMatcher(EndpointRequest.toAnyEndpoint())
.authorizeRequests(requests -> requests.anyRequest().hasRole("ENDPOINT_ADMIN"));
http.httpBasic();
return http.build();
}
} }

View File

@ -5,6 +5,7 @@ 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.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.dto.req.AuthorRegisterReqDto; import io.github.xxyopen.novel.dto.req.AuthorRegisterReqDto;
import io.github.xxyopen.novel.dto.req.BookAddReqDto; import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto; import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
@ -12,9 +13,19 @@ import io.github.xxyopen.novel.dto.resp.BookChapterRespDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto; import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.AuthorService; import io.github.xxyopen.novel.service.AuthorService;
import io.github.xxyopen.novel.service.BookService; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springdoc.core.annotations.ParameterObject;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 作家后台-作家模块 API 控制器 * 作家后台-作家模块 API 控制器
@ -22,6 +33,8 @@ import org.springframework.web.bind.annotation.*;
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/23 * @date 2022/5/23
*/ */
@Tag(name = "AuthorController", description = "作家后台-作者模块")
@SecurityRequirement(name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME)
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_AUTHOR_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_AUTHOR_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@ -34,6 +47,7 @@ public class AuthorController {
/** /**
* 作家注册接口 * 作家注册接口
*/ */
@Operation(summary = "作家注册接口")
@PostMapping("register") @PostMapping("register")
public RestResp<Void> register(@Valid @RequestBody AuthorRegisterReqDto dto) { public RestResp<Void> register(@Valid @RequestBody AuthorRegisterReqDto dto) {
dto.setUserId(UserHolder.getUserId()); dto.setUserId(UserHolder.getUserId());
@ -43,6 +57,7 @@ public class AuthorController {
/** /**
* 查询作家状态接口 * 查询作家状态接口
*/ */
@Operation(summary = "作家状态查询接口")
@GetMapping("status") @GetMapping("status")
public RestResp<Integer> getStatus() { public RestResp<Integer> getStatus() {
return authorService.getStatus(UserHolder.getUserId()); return authorService.getStatus(UserHolder.getUserId());
@ -51,6 +66,7 @@ public class AuthorController {
/** /**
* 小说发布接口 * 小说发布接口
*/ */
@Operation(summary = "小说发布接口")
@PostMapping("book") @PostMapping("book")
public RestResp<Void> publishBook(@Valid @RequestBody BookAddReqDto dto) { public RestResp<Void> publishBook(@Valid @RequestBody BookAddReqDto dto) {
return bookService.saveBook(dto); return bookService.saveBook(dto);
@ -59,16 +75,20 @@ public class AuthorController {
/** /**
* 小说发布列表查询接口 * 小说发布列表查询接口
*/ */
@Operation(summary = "小说发布列表查询接口")
@GetMapping("books") @GetMapping("books")
public RestResp<PageRespDto<BookInfoRespDto>> listBooks(PageReqDto dto) { public RestResp<PageRespDto<BookInfoRespDto>> listBooks(@ParameterObject PageReqDto dto) {
return bookService.listAuthorBooks(dto); return bookService.listAuthorBooks(dto);
} }
/** /**
* 小说章节发布接口 * 小说章节发布接口
*/ */
@Operation(summary = "小说章节发布接口")
@PostMapping("book/chapter/{bookId}") @PostMapping("book/chapter/{bookId}")
public RestResp<Void> publishBookChapter(@PathVariable("bookId") Long bookId, @Valid @RequestBody ChapterAddReqDto dto) { public RestResp<Void> publishBookChapter(
@Parameter(description = "小说ID") @PathVariable("bookId") Long bookId,
@Valid @RequestBody ChapterAddReqDto dto) {
dto.setBookId(bookId); dto.setBookId(bookId);
return bookService.saveBookChapter(dto); return bookService.saveBookChapter(dto);
} }
@ -76,8 +96,11 @@ public class AuthorController {
/** /**
* 小说章节发布列表查询接口 * 小说章节发布列表查询接口
*/ */
@Operation(summary = "小说章节发布列表查询接口")
@GetMapping("book/chapters/{bookId}") @GetMapping("book/chapters/{bookId}")
public RestResp<PageRespDto<BookChapterRespDto>> listBookChapters(@PathVariable("bookId") Long bookId, PageReqDto dto) { public RestResp<PageRespDto<BookChapterRespDto>> listBookChapters(
@Parameter(description = "小说ID") @PathVariable("bookId") Long bookId,
@ParameterObject PageReqDto dto) {
return bookService.listBookChapters(bookId, dto); return bookService.listBookChapters(bookId, dto);
} }

View File

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

View File

@ -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.HomeBookRespDto;
import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto; import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto;
import io.github.xxyopen.novel.service.HomeService; import io.github.xxyopen.novel.service.HomeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -18,6 +20,7 @@ import java.util.List;
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/12 * @date 2022/5/12
*/ */
@Tag(name = "HomeController", description = "前台门户-首页模块")
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_FRONT_HOME_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_FRONT_HOME_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@ -28,6 +31,7 @@ public class HomeController {
/** /**
* 首页小说推荐查询接口 * 首页小说推荐查询接口
*/ */
@Operation(summary = "首页小说推荐查询接口")
@GetMapping("books") @GetMapping("books")
public RestResp<List<HomeBookRespDto>> listHomeBooks() { public RestResp<List<HomeBookRespDto>> listHomeBooks() {
return homeService.listHomeBooks(); return homeService.listHomeBooks();
@ -36,6 +40,7 @@ public class HomeController {
/** /**
* 首页友情链接列表查询接口 * 首页友情链接列表查询接口
*/ */
@Operation(summary = "首页友情链接列表查询接口")
@GetMapping("friend_Link/list") @GetMapping("friend_Link/list")
public RestResp<List<HomeFriendLinkRespDto>> listHomeFriendLinks() { public RestResp<List<HomeFriendLinkRespDto>> listHomeFriendLinks() {
return homeService.listHomeFriendLinks(); return homeService.listHomeFriendLinks();

View File

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

View File

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

View File

@ -6,7 +6,10 @@ import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto; import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto; import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.SearchService; 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 lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -17,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/27 * @date 2022/5/27
*/ */
@Tag(name = "SearchController", description = "前台门户-搜索模块")
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_FRONT_SEARCH_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_FRONT_SEARCH_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@ -27,8 +31,10 @@ public class SearchController {
/** /**
* 小说搜索接口 * 小说搜索接口
*/ */
@Operation(summary = "小说搜索接口")
@GetMapping("books") @GetMapping("books")
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) { public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(
@ParameterObject BookSearchReqDto condition) {
return searchService.searchBooks(condition); return searchService.searchBooks(condition);
} }

View File

@ -3,6 +3,7 @@ package io.github.xxyopen.novel.controller.front;
import io.github.xxyopen.novel.core.auth.UserHolder; import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts; import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto; import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.req.UserInfoUptReqDto; import io.github.xxyopen.novel.dto.req.UserInfoUptReqDto;
import io.github.xxyopen.novel.dto.req.UserLoginReqDto; import io.github.xxyopen.novel.dto.req.UserLoginReqDto;
@ -12,9 +13,20 @@ import io.github.xxyopen.novel.dto.resp.UserLoginRespDto;
import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto; import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto;
import io.github.xxyopen.novel.service.BookService; import io.github.xxyopen.novel.service.BookService;
import io.github.xxyopen.novel.service.UserService; import io.github.xxyopen.novel.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 前台门户-会员模块 API 控制器 * 前台门户-会员模块 API 控制器
@ -22,6 +34,8 @@ import org.springframework.web.bind.annotation.*;
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/17 * @date 2022/5/17
*/ */
@Tag(name = "UserController", description = "前台门户-会员模块")
@SecurityRequirement(name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME)
@RestController @RestController
@RequestMapping(ApiRouterConsts.API_FRONT_USER_URL_PREFIX) @RequestMapping(ApiRouterConsts.API_FRONT_USER_URL_PREFIX)
@RequiredArgsConstructor @RequiredArgsConstructor
@ -34,6 +48,7 @@ public class UserController {
/** /**
* 用户注册接口 * 用户注册接口
*/ */
@Operation(summary = "用户注册接口")
@PostMapping("register") @PostMapping("register")
public RestResp<UserRegisterRespDto> register(@Valid @RequestBody UserRegisterReqDto dto) { public RestResp<UserRegisterRespDto> register(@Valid @RequestBody UserRegisterReqDto dto) {
return userService.register(dto); return userService.register(dto);
@ -42,6 +57,7 @@ public class UserController {
/** /**
* 用户登录接口 * 用户登录接口
*/ */
@Operation(summary = "用户登录接口")
@PostMapping("login") @PostMapping("login")
public RestResp<UserLoginRespDto> login(@Valid @RequestBody UserLoginReqDto dto) { public RestResp<UserLoginRespDto> login(@Valid @RequestBody UserLoginReqDto dto) {
return userService.login(dto); return userService.login(dto);
@ -50,6 +66,7 @@ public class UserController {
/** /**
* 用户信息查询接口 * 用户信息查询接口
*/ */
@Operation(summary = "用户信息查询接口")
@GetMapping @GetMapping
public RestResp<UserInfoRespDto> getUserInfo() { public RestResp<UserInfoRespDto> getUserInfo() {
return userService.getUserInfo(UserHolder.getUserId()); return userService.getUserInfo(UserHolder.getUserId());
@ -58,6 +75,7 @@ public class UserController {
/** /**
* 用户信息修改接口 * 用户信息修改接口
*/ */
@Operation(summary = "用户信息修改接口")
@PutMapping @PutMapping
public RestResp<Void> updateUserInfo(@Valid @RequestBody UserInfoUptReqDto dto) { public RestResp<Void> updateUserInfo(@Valid @RequestBody UserInfoUptReqDto dto) {
dto.setUserId(UserHolder.getUserId()); dto.setUserId(UserHolder.getUserId());
@ -67,6 +85,7 @@ public class UserController {
/** /**
* 用户反馈提交接口 * 用户反馈提交接口
*/ */
@Operation(summary = "用户反馈提交接口")
@PostMapping("feedback") @PostMapping("feedback")
public RestResp<Void> submitFeedback(@RequestBody String content) { public RestResp<Void> submitFeedback(@RequestBody String content) {
return userService.saveFeedback(UserHolder.getUserId(), content); return userService.saveFeedback(UserHolder.getUserId(), content);
@ -75,14 +94,16 @@ public class UserController {
/** /**
* 用户反馈删除接口 * 用户反馈删除接口
*/ */
@Operation(summary = "用户反馈删除接口")
@DeleteMapping("feedback/{id}") @DeleteMapping("feedback/{id}")
public RestResp<Void> deleteFeedback(@PathVariable Long id) { public RestResp<Void> deleteFeedback(@Parameter(description = "反馈ID") @PathVariable Long id) {
return userService.deleteFeedback(UserHolder.getUserId(), id); return userService.deleteFeedback(UserHolder.getUserId(), id);
} }
/** /**
* 发表评论接口 * 发表评论接口
*/ */
@Operation(summary = "发表评论接口")
@PostMapping("comment") @PostMapping("comment")
public RestResp<Void> comment(@Valid @RequestBody UserCommentReqDto dto) { public RestResp<Void> comment(@Valid @RequestBody UserCommentReqDto dto) {
dto.setUserId(UserHolder.getUserId()); dto.setUserId(UserHolder.getUserId());
@ -92,26 +113,28 @@ public class UserController {
/** /**
* 修改评论接口 * 修改评论接口
*/ */
@Operation(summary = "修改评论接口")
@PutMapping("comment/{id}") @PutMapping("comment/{id}")
public RestResp<Void> updateComment(@PathVariable Long id, String content) { public RestResp<Void> updateComment(@Parameter(description = "评论ID") @PathVariable Long id,
String content) {
return bookService.updateComment(UserHolder.getUserId(), id, content); return bookService.updateComment(UserHolder.getUserId(), id, content);
} }
/** /**
* 删除评论接口 * 删除评论接口
*/ */
@Operation(summary = "删除评论接口")
@DeleteMapping("comment/{id}") @DeleteMapping("comment/{id}")
public RestResp<Void> deleteComment(@PathVariable Long id) { public RestResp<Void> deleteComment(@Parameter(description = "评论ID") @PathVariable Long id) {
return bookService.deleteComment(UserHolder.getUserId(), id); return bookService.deleteComment(UserHolder.getUserId(), id);
} }
/** /**
* 查询书架状态接口 * 查询书架状态接口 0-不在书架 1-已在书架
* 0-不在书架
* 1-已在书架
*/ */
@Operation(summary = "查询书架状态接口")
@GetMapping("bookshelf_status") @GetMapping("bookshelf_status")
public RestResp<Integer> getBookshelfStatus(@RequestBody String bookId) { public RestResp<Integer> getBookshelfStatus(@Parameter(description = "小说ID") String bookId) {
return userService.getBookshelfStatus(UserHolder.getUserId(), bookId); return userService.getBookshelfStatus(UserHolder.getUserId(), bookId);
} }

View File

@ -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 "";
}

View File

@ -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;
}

View File

@ -0,0 +1,81 @@
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 java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.concurrent.TimeUnit;
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;
/**
* 分布式锁 切面
*
* @author xiongxiaoyang
* @date 2022/6/20
*/
@Aspect
@Component
public record LockAspect(RedissonClient redissonClient) {
private static final String KEY_PREFIX = "Lock";
private static final String KEY_SEPARATOR = "::";
@Around(value = "@annotation(io.github.xxyopen.novel.core.annotation.Lock)")
@SneakyThrows
public Object doAround(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
Lock lock = targetMethod.getAnnotation(Lock.class);
String lockKey = KEY_PREFIX + buildLockKey(lock.prefix(), targetMethod,
joinPoint.getArgs());
RLock rLock = redissonClient.getLock(lockKey);
if (lock.isWait() ? rLock.tryLock(lock.waitTime(), TimeUnit.SECONDS) : rLock.tryLock()) {
try {
return joinPoint.proceed();
} finally {
rLock.unlock();
}
}
throw new BusinessException(lock.failCode());
}
private String buildLockKey(String prefix, Method method, Object[] args) {
StringBuilder builder = new StringBuilder();
if (StringUtils.hasText(prefix)) {
builder.append(KEY_SEPARATOR).append(prefix);
}
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
builder.append(KEY_SEPARATOR);
if (parameters[i].isAnnotationPresent(Key.class)) {
Key key = parameters[i].getAnnotation(Key.class);
builder.append(parseKeyExpr(key.expr(), args[i]));
}
}
return builder.toString();
}
private String parseKeyExpr(String expr, Object arg) {
if (!StringUtils.hasText(expr)) {
return arg.toString();
}
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(expr, new TemplateParserContext());
return expression.getValue(arg, String.class);
}
}

View File

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

View File

@ -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.core.util.JwtUtils;
import io.github.xxyopen.novel.dto.UserInfoDto; import io.github.xxyopen.novel.dto.UserInfoDto;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager; import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager;
import org.springframework.util.StringUtils;
import java.util.Objects; import java.util.Objects;
import org.springframework.util.StringUtils;
/** /**
* 策略模式实现用户认证授权功能 * 策略模式实现用户认证授权功能
@ -19,23 +18,24 @@ import java.util.Objects;
public interface AuthStrategy { public interface AuthStrategy {
/** /**
* 请求用户认证 * 用户认证授权
* *
* @param token 登录 token * @param token 登录 token
* @param requestUri 请求的 URI * @param requestUri 请求的 URI
* @throws BusinessException 认证失败则抛出业务异常 * @throws BusinessException 认证失败则抛出业务异常
*/ */
void auth(String token, String requestUri) throws BusinessException; void auth(String token, String requestUri) throws BusinessException;
/** /**
* 前台多系统单点登录统一账号认证(门户系统、作家系统以及后面会扩展的漫画系统和视频系统等) * 前台多系统单点登录统一账号认证授权(门户系统、作家系统以及后面会扩展的漫画系统和视频系统等)
* *
* @param jwtUtils jwt 工具 * @param jwtUtils jwt 工具
* @param userInfoCacheManager 用户缓存管理对象 * @param userInfoCacheManager 用户缓存管理对象
* @param token token 登录 token * @param token token 登录 token
* @return 用户ID * @return 用户ID
*/ */
default Long authSSO(JwtUtils jwtUtils, UserInfoCacheManager userInfoCacheManager, String token) { default Long authSSO(JwtUtils jwtUtils, UserInfoCacheManager userInfoCacheManager,
String token) {
if (!StringUtils.hasText(token)) { if (!StringUtils.hasText(token)) {
// token 为空 // token 为空
throw new BusinessException(ErrorCodeEnum.USER_LOGIN_EXPIRED); throw new BusinessException(ErrorCodeEnum.USER_LOGIN_EXPIRED);
@ -55,4 +55,5 @@ public interface AuthStrategy {
// 返回 userId // 返回 userId
return userId; return userId;
} }
} }

View File

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

View File

@ -7,7 +7,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* 前台门户系统 认证策略 * 前台门户系统 认证授权策略
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/18 * @date 2022/5/18
@ -23,6 +23,7 @@ public class FrontAuthStrategy implements AuthStrategy {
@Override @Override
public void auth(String token, String requestUri) throws BusinessException { public void auth(String token, String requestUri) throws BusinessException {
// 统一账号认证 // 统一账号认证
authSSO(jwtUtils,userInfoCacheManager,token); authSSO(jwtUtils, userInfoCacheManager, token);
} }
} }

View File

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

View File

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

View File

@ -5,15 +5,12 @@ import lombok.Getter;
/** /**
* 错误码枚举类。 * 错误码枚举类。
* * <p>
* 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。 * 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。 错误产生来源分为 A/B/C A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 超时等问题; B
* 错误产生来源分为 A/B/C A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 * 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题; C 表示错误来源 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999大类之间的
* 超时等问题; B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题; C 表示错误来源
* 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999大类之间的
* 步长间距预留 100。 * 步长间距预留 100。
* * <p>
* 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。 * 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码。
* 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码。
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/11 * @date 2022/5/11
@ -24,134 +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_EXCEPTION("A0500", "用户请求服务异常"),
/** /**
* 请求超出限制 * 请求超出限制
* */ */
USER_REQ_MANY("A0501","请求超出限制"), USER_REQ_MANY("A0501", "请求超出限制"),
/** /**
* 用户评论异常 * 用户评论异常
* */ */
USER_COMMENT("A2000","用户评论异常"), USER_COMMENT("A2000", "用户评论异常"),
/** /**
* 用户评论异常 * 用户评论异常
* */ */
USER_COMMENTED("A2001","用户已发表评论"), USER_COMMENTED("A2001", "用户已发表评论"),
/** /**
* 作家发布异常 * 作家发布异常
* */ */
AUTHOR_PUBLISH("A3000","作家发布异常"), AUTHOR_PUBLISH("A3000", "作家发布异常"),
/** /**
* 小说名已存在 * 小说名已存在
* */ */
AUTHOR_BOOK_NAME_EXIST("A3001","小说名已存在"), 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 final String code; private final String code;
/** /**
* 中文描述 * 中文描述
* */ */
private final String message; private final String message;
} }

View File

@ -19,28 +19,28 @@ public class CommonExceptionHandler {
/** /**
* 处理数据校验异常 * 处理数据校验异常
* */ */
@ExceptionHandler(BindException.class) @ExceptionHandler(BindException.class)
public RestResp<Void> handlerBindException(BindException e){ public RestResp<Void> handlerBindException(BindException e) {
log.error(e.getMessage(),e); log.error(e.getMessage(), e);
return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR); return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR);
} }
/** /**
* 处理业务异常 * 处理业务异常
* */ */
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
public RestResp<Void> handlerBusinessException(BusinessException e){ public RestResp<Void> handlerBusinessException(BusinessException e) {
log.error(e.getMessage(),e); log.error(e.getMessage(), e);
return RestResp.fail(e.getErrorCodeEnum()); return RestResp.fail(e.getErrorCodeEnum());
} }
/** /**
* 处理系统异常 * 处理系统异常
* */ */
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public RestResp<Void> handlerException(Exception e){ public RestResp<Void> handlerException(Exception e) {
log.error(e.getMessage(),e); log.error(e.getMessage(), e);
return RestResp.error(); return RestResp.error();
} }

View File

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

View File

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

View File

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

View File

@ -1,14 +1,15 @@
package io.github.xxyopen.novel.core.common.util; package io.github.xxyopen.novel.core.common.util;
import lombok.experimental.UtilityClass; import java.awt.Color;
import java.awt.Font;
import javax.imageio.ImageIO; import java.awt.Graphics;
import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Base64; import java.util.Base64;
import java.util.Random; import java.util.Random;
import javax.imageio.ImageIO;
import lombok.experimental.UtilityClass;
/** /**
* 图片验证码工具类 * 图片验证码工具类
@ -79,7 +80,7 @@ public class ImgVerifyCodeUtils {
for (int i = 1; i <= verifyCode.length(); i++) { for (int i = 1; i <= verifyCode.length(); i++) {
g.setFont(getFont()); g.setFont(getFont());
g.setColor(new Color(random.nextInt(101), random.nextInt(111), random g.setColor(new Color(random.nextInt(101), random.nextInt(111), random
.nextInt(121))); .nextInt(121)));
g.translate(random.nextInt(3), random.nextInt(3)); g.translate(random.nextInt(3), random.nextInt(3));
g.drawString(String.valueOf(verifyCode.charAt(i - 1)), 13 * i, 23); g.drawString(String.valueOf(verifyCode.charAt(i - 1)), 13 * i, 23);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ public class EsConfig {
// Create the transport with a Jackson mapper // Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport( ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper()); restClient, new JacksonJsonpMapper());
// And create the API client // And create the API client
return new ElasticsearchClient(transport); return new ElasticsearchClient(transport);

View File

@ -0,0 +1,26 @@
package io.github.xxyopen.novel.core.config;
import lombok.SneakyThrows;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Redisson 配置类
*
* @author xiongxiaoyang
* @date 2022/6/20
*/
@Configuration
public class RedissonConfig {
@Bean
@SneakyThrows
public RedissonClient redissonClient() {
Config config = Config.fromYAML(getClass().getResource("/redisson.yml"));
return Redisson.create(config);
}
}

View File

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

View File

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

View File

@ -10,22 +10,22 @@ public class AmqpConsts {
/** /**
* 小说信息改变 MQ * 小说信息改变 MQ
* */ */
public static class BookChangeMq{ public static class BookChangeMq {
/** /**
* 小说信息改变交换机 * 小说信息改变交换机
* */ */
public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE"; public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE";
/** /**
* Elasticsearch book 索引更新的队列 * Elasticsearch book 索引更新的队列
* */ */
public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE"; public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE";
/** /**
* Redis book 缓存更新的队列 * Redis book 缓存更新的队列
* */ */
public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE"; public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE";
} }

View File

@ -34,32 +34,32 @@ public class ApiRouterConsts {
/** /**
* 首页模块请求路径前缀 * 首页模块请求路径前缀
* */ */
public static final String HOME_URL_PREFIX = "/home"; public static final String HOME_URL_PREFIX = "/home";
/** /**
* 首页模块请求路径前缀 * 首页模块请求路径前缀
* */ */
public static final String NEWS_URL_PREFIX = "/news"; public static final String NEWS_URL_PREFIX = "/news";
/** /**
* 小说模块请求路径前缀 * 小说模块请求路径前缀
* */ */
public static final String BOOK_URL_PREFIX = "/book"; public static final String BOOK_URL_PREFIX = "/book";
/** /**
* 会员模块请求路径前缀 * 会员模块请求路径前缀
* */ */
public static final String USER_URL_PREFIX = "/user"; public static final String USER_URL_PREFIX = "/user";
/** /**
* 资源(图片/视频/文档)模块请求路径前缀 * 资源(图片/视频/文档)模块请求路径前缀
* */ */
public static final String RESOURCE_URL_PREFIX = "/resource"; public static final String RESOURCE_URL_PREFIX = "/resource";
/** /**
* 搜索模块请求路径前缀 * 搜索模块请求路径前缀
* */ */
public static final String SEARCH_URL_PREFIX = "/search"; public static final String SEARCH_URL_PREFIX = "/search";
/** /**
@ -85,11 +85,13 @@ public class ApiRouterConsts {
/** /**
* 前台门户资源(图片/视频/文档相关API请求路径前缀 * 前台门户资源(图片/视频/文档相关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请求路径前缀 * 前台门户搜索相关API请求路径前缀
* */ */
public static final String API_FRONT_SEARCH_URL_PREFIX = API_FRONT_URL_PREFIX + SEARCH_URL_PREFIX; public static final String API_FRONT_SEARCH_URL_PREFIX =
API_FRONT_URL_PREFIX + SEARCH_URL_PREFIX;
} }

View File

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

View File

@ -10,7 +10,6 @@ import lombok.Getter;
*/ */
public class DatabaseConsts { public class DatabaseConsts {
/** /**
* 用户信息表 * 用户信息表
*/ */

View File

@ -14,8 +14,8 @@ public class EsConsts {
/** /**
* 小说索引 * 小说索引
* */ */
public static class BookIndex{ public static class BookIndex {
private BookIndex() { private BookIndex() {
throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG); throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
@ -23,18 +23,18 @@ public class EsConsts {
/** /**
* 索引名 * 索引名
* */ */
public static final String INDEX_NAME = "book"; public static final String INDEX_NAME = "book";
/** /**
* id * id
*/ */
public static final String FIELD_ID = "id"; public static final String FIELD_ID = "id";
/** /**
* 作品方向;0-男频 1-女频 * 作品方向;0-男频 1-女频
*/ */
public static final String FIELD_WORK_DIRECTION = "workDirection"; public static final String FIELD_WORK_DIRECTION = "workDirection";
/** /**
* 类别ID * 类别ID
@ -110,7 +110,7 @@ public class EsConsts {
* 是否收费;1-收费 0-免费 * 是否收费;1-收费 0-免费
*/ */
public static final String FIELD_IS_VIP = "isVip"; public static final String FIELD_IS_VIP = "isVip";
} }
} }

View File

@ -14,32 +14,32 @@ public class SystemConfigConsts {
/** /**
* Http 请求认证 Header * Http 请求认证 Header
* */ */
public static final String HTTP_AUTH_HEADER_NAME = "Authorization"; public static final String HTTP_AUTH_HEADER_NAME = "Authorization";
/** /**
* 前台门户系统标识 * 前台门户系统标识
* */ */
public static final String NOVEL_FRONT_KEY = "front"; public static final String NOVEL_FRONT_KEY = "front";
/** /**
* 作家管理系统标识 * 作家管理系统标识
* */ */
public static final String NOVEL_AUTHOR_KEY = "author"; public static final String NOVEL_AUTHOR_KEY = "author";
/** /**
* 后台管理系统标识 * 后台管理系统标识
* */ */
public static final String NOVEL_ADMIN_KEY = "admin"; public static final String NOVEL_ADMIN_KEY = "admin";
/** /**
* 图片上传目录 * 图片上传目录
* */ */
public static final String IMAGE_UPLOAD_DIRECTORY = "/image/"; public static final String IMAGE_UPLOAD_DIRECTORY = "/image/";
/** /**
* 常量类实例化异常信息 * 常量类实例化异常信息
* */ */
public static final String CONST_INSTANCE_EXCEPTION_MSG = "Constant class"; public static final String CONST_INSTANCE_EXCEPTION_MSG = "Constant class";
} }

View File

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

View File

@ -9,18 +9,16 @@ import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts; import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/** /**
* 认证 拦截器 * 认证授权 拦截器:为了注入其它的 Spring beans需要通过 @Component 注解将该拦截器注册到 Spring 上下文
* 为了注入其它的 Spring beans需要通过 @Component 注解将该拦截器注册到 Spring 上下文
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/18 * @date 2022/5/18
@ -29,13 +27,14 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor { public class AuthInterceptor implements HandlerInterceptor {
private final Map<String,AuthStrategy> authStrategy; private final Map<String, AuthStrategy> authStrategy;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 获取登录 JWT // 获取登录 JWT
String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME); String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);
@ -44,25 +43,27 @@ public class AuthInterceptor implements HandlerInterceptor {
// 根据请求的 URI 得到认证策略 // 根据请求的 URI 得到认证策略
String subUri = requestUri.substring(ApiRouterConsts.API_URL_PREFIX.length() + 1); String subUri = requestUri.substring(ApiRouterConsts.API_URL_PREFIX.length() + 1);
String systemName = subUri.substring(0,subUri.indexOf("/")); String systemName = subUri.substring(0, subUri.indexOf("/"));
String authStrategyName = String.format("%sAuthStrategy",systemName); String authStrategyName = String.format("%sAuthStrategy", systemName);
// 开始认证 // 开始认证
try { try {
authStrategy.get(authStrategyName).auth(token,requestUri); authStrategy.get(authStrategyName).auth(token, requestUri);
return HandlerInterceptor.super.preHandle(request, response, handler); return HandlerInterceptor.super.preHandle(request, response, handler);
}catch (BusinessException exception){ } catch (BusinessException exception) {
// 认证失败 // 认证失败
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(objectMapper.writeValueAsString(RestResp.fail(exception.getErrorCodeEnum()))); response.getWriter().write(
objectMapper.writeValueAsString(RestResp.fail(exception.getErrorCodeEnum())));
return false; return false;
} }
} }
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
@Override @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 {
// 清理当前线程保存的用户数据 // 清理当前线程保存的用户数据
UserHolder.clear(); UserHolder.clear();
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);

View File

@ -2,15 +2,14 @@ package io.github.xxyopen.novel.core.interceptor;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; 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") @SuppressWarnings("NullableProblems")
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 获取请求的 URI // 获取请求的 URI
String requestUri = request.getRequestURI(); String requestUri = request.getRequestURI();
// 缓存10天 // 缓存10天
response.setDateHeader("expires", System.currentTimeMillis() + 60 * 60 * 24 * 10 * 1000); 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]; byte[] b = new byte[4096];
for (int n; (n = input.read(b)) != -1; ) { for (int n; (n = input.read(b)) != -1; ) {
out.write(b, 0, n); out.write(b, 0, n);

View File

@ -15,20 +15,18 @@ import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.common.util.IpUtils; import io.github.xxyopen.novel.core.common.util.IpUtils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** /**
* 流量限制 拦截器 * 流量限制 拦截器:实现接口防刷和限流
* 实现接口防刷和限流
* *
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/6/1 * @date 2022/6/1
@ -59,18 +57,19 @@ public class FlowLimitInterceptor implements HandlerInterceptor {
// 接口防刷规则 1所有的请求限制每个 IP 每秒最多只能通过 50 个,超出限制直接拒绝 // 接口防刷规则 1所有的请求限制每个 IP 每秒最多只能通过 50 个,超出限制直接拒绝
ParamFlowRule rule2 = new ParamFlowRule(NOVEL_RESOURCE) ParamFlowRule rule2 = new ParamFlowRule(NOVEL_RESOURCE)
.setParamIdx(0) .setParamIdx(0)
.setCount(50); .setCount(50);
// 接口防刷规则 2所有的请求限制每个 IP 每分钟最多只能通过 1000 个,超出限制直接拒绝 // 接口防刷规则 2所有的请求限制每个 IP 每分钟最多只能通过 1000 个,超出限制直接拒绝
ParamFlowRule rule3 = new ParamFlowRule(NOVEL_RESOURCE) ParamFlowRule rule3 = new ParamFlowRule(NOVEL_RESOURCE)
.setParamIdx(0) .setParamIdx(0)
.setCount(1000) .setCount(1000)
.setDurationInSec(60); .setDurationInSec(60);
ParamFlowRuleManager.loadRules(Arrays.asList(rule2, rule3)); ParamFlowRuleManager.loadRules(Arrays.asList(rule2, rule3));
} }
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String ip = IpUtils.getRealIp(request); String ip = IpUtils.getRealIp(request);
Entry entry = null; Entry entry = null;
try { try {
@ -85,7 +84,8 @@ public class FlowLimitInterceptor implements HandlerInterceptor {
log.info("IP:{}被限流了!", ip); log.info("IP:{}被限流了!", ip);
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(objectMapper.writeValueAsString(RestResp.fail(ErrorCodeEnum.USER_REQ_MANY))); response.getWriter()
.write(objectMapper.writeValueAsString(RestResp.fail(ErrorCodeEnum.USER_REQ_MANY)));
} finally { } finally {
// 注意exit 的时候也一定要带上对应的参数,否则可能会有统计错误。 // 注意exit 的时候也一定要带上对应的参数,否则可能会有统计错误。
if (entry != null) { if (entry != null) {

View File

@ -24,7 +24,8 @@ public class TokenParseInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils; private final JwtUtils jwtUtils;
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 获取登录 JWT // 获取登录 JWT
String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME); String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);
if (StringUtils.hasText(token)) { if (StringUtils.hasText(token)) {
@ -35,7 +36,8 @@ public class TokenParseInterceptor implements HandlerInterceptor {
} }
@Override @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 {
// 清理当前线程保存的用户数据 // 清理当前线程保存的用户数据
UserHolder.clear(); UserHolder.clear();
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);

View File

@ -3,9 +3,8 @@ package io.github.xxyopen.novel.core.json.deserializer;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonDeserializer;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException; import java.io.IOException;
import org.springframework.boot.jackson.JsonComponent;
/** /**
@ -18,16 +17,16 @@ import java.io.IOException;
public class GlobalJsonDeserializer { public class GlobalJsonDeserializer {
/** /**
* 字符串反序列化器 * 字符串反序列化器:过滤特殊字符,解决 XSS 攻击
* 过滤特殊字符,解决 XSS 攻击
*/ */
public static class StringDeserializer extends JsonDeserializer<String> { public static class StringDeserializer extends JsonDeserializer<String> {
@Override @Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { public String deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
return jsonParser.getValueAsString() return jsonParser.getValueAsString()
.replace("<", "&lt;") .replace("<", "&lt;")
.replace(">", "&gt;"); .replace(">", "&gt;");
} }
} }
} }

View File

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

View File

@ -21,7 +21,8 @@ import org.springframework.stereotype.Component;
* @date 2022/5/25 * @date 2022/5/25
*/ */
@Component @Component
@ConditionalOnProperty(prefix = "spring", name = {"elasticsearch.enable","amqp.enable"}, havingValue = "true") @ConditionalOnProperty(prefix = "spring", name = {"elasticsearch.enable",
"amqp.enable"}, havingValue = "true")
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class RabbitQueueListener { public class RabbitQueueListener {
@ -32,15 +33,15 @@ public class RabbitQueueListener {
/** /**
* 监听小说信息改变的 ES 更新队列,更新最新小说信息到 ES * 监听小说信息改变的 ES 更新队列,更新最新小说信息到 ES
* */ */
@RabbitListener(queues = AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE) @RabbitListener(queues = AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE)
@SneakyThrows @SneakyThrows
public void updateEsBook(Long bookId) { public void updateEsBook(Long bookId) {
BookInfo bookInfo = bookInfoMapper.selectById(bookId); BookInfo bookInfo = bookInfoMapper.selectById(bookId);
IndexResponse response = esClient.index(i -> i IndexResponse response = esClient.index(i -> i
.index(EsConsts.BookIndex.INDEX_NAME) .index(EsConsts.BookIndex.INDEX_NAME)
.id(bookInfo.getId().toString()) .id(bookInfo.getId().toString())
.document(EsBookDto.build(bookInfo)) .document(EsBookDto.build(bookInfo))
); );
log.info("Indexed with version " + response.version()); log.info("Indexed with version " + response.version());
} }

View File

@ -13,14 +13,13 @@ import io.github.xxyopen.novel.core.constant.EsConsts;
import io.github.xxyopen.novel.dao.entity.BookInfo; import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper; import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.es.EsBookDto; import io.github.xxyopen.novel.dto.es.EsBookDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 小说数据同步到 elasticsearch 任务 * 小说数据同步到 elasticsearch 任务
* *
@ -51,10 +50,10 @@ public class BookToEsTask {
for (; ; ) { for (; ; ) {
queryWrapper.clear(); queryWrapper.clear();
queryWrapper queryWrapper
.orderByAsc(DatabaseConsts.CommonColumnEnum.ID.getName()) .orderByAsc(DatabaseConsts.CommonColumnEnum.ID.getName())
.gt(DatabaseConsts.CommonColumnEnum.ID.getName(), maxId) .gt(DatabaseConsts.CommonColumnEnum.ID.getName(), maxId)
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0) .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.last(DatabaseConsts.SqlEnum.LIMIT_30.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_30.getSql());
bookInfos = bookInfoMapper.selectList(queryWrapper); bookInfos = bookInfoMapper.selectList(queryWrapper);
if (bookInfos.isEmpty()) { if (bookInfos.isEmpty()) {
break; break;
@ -63,11 +62,11 @@ public class BookToEsTask {
for (BookInfo book : bookInfos) { for (BookInfo book : bookInfos) {
br.operations(op -> op br.operations(op -> op
.index(idx -> idx .index(idx -> idx
.index(EsConsts.BookIndex.INDEX_NAME) .index(EsConsts.BookIndex.INDEX_NAME)
.id(book.getId().toString()) .id(book.getId().toString())
.document(EsBookDto.build(book)) .document(EsBookDto.build(book))
) )
).timeout(Time.of(t -> t.time("10s"))); ).timeout(Time.of(t -> t.time("10s")));
maxId = book.getId(); maxId = book.getId();
} }

View File

@ -5,14 +5,13 @@ import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/** /**
* JWT 工具类 * JWT 工具类
* *
@ -37,21 +36,23 @@ public class JwtUtils {
/** /**
* 根据用户ID生成JWT * 根据用户ID生成JWT
* @param uid 用户ID *
* @param uid 用户ID
* @param systemKey 系统标识 * @param systemKey 系统标识
* @return JWT * @return JWT
*/ */
public String generateToken(Long uid, String systemKey) { public String generateToken(Long uid, String systemKey) {
return Jwts.builder() return Jwts.builder()
.setHeaderParam(HEADER_SYSTEM_KEY, systemKey) .setHeaderParam(HEADER_SYSTEM_KEY, systemKey)
.setSubject(uid.toString()) .setSubject(uid.toString())
.signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
.compact(); .compact();
} }
/** /**
* 解析JWT返回用户ID * 解析JWT返回用户ID
* @param token JWT *
* @param token JWT
* @param systemKey 系统标识 * @param systemKey 系统标识
* @return 用户ID * @return 用户ID
*/ */
@ -59,9 +60,9 @@ public class JwtUtils {
Jws<Claims> claimsJws; Jws<Claims> claimsJws;
try { try {
claimsJws = Jwts.parserBuilder() claimsJws = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
.build() .build()
.parseClaimsJws(token); .parseClaimsJws(token);
// OK, we can trust this JWT // OK, we can trust this JWT
// 判断该 JWT 是否属于指定系统 // 判断该 JWT 是否属于指定系统
if (Objects.equals(claimsJws.getHeader().get(HEADER_SYSTEM_KEY), systemKey)) { if (Objects.equals(claimsJws.getHeader().get(HEADER_SYSTEM_KEY), systemKey)) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package io.github.xxyopen.novel.dto.req; package io.github.xxyopen.novel.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
@ -17,17 +18,20 @@ public class ChapterAddReqDto {
/** /**
* 小说ID * 小说ID
*/ */
@Schema(description = "小说ID", required = true)
private Long bookId; private Long bookId;
/** /**
* 章节名 * 章节名
*/ */
@NotBlank @NotBlank
@Schema(description = "章节名", required = true)
private String chapterName; private String chapterName;
/** /**
* 章节内容 * 章节内容
*/ */
@Schema(description = "章节内容", required = true)
@NotBlank @NotBlank
@Length(min = 50) @Length(min = 50)
private String chapterContent; private String chapterContent;
@ -35,6 +39,7 @@ public class ChapterAddReqDto {
/** /**
* 是否收费;1-收费 0-免费 * 是否收费;1-收费 0-免费
*/ */
@Schema(description = "是否收费;1-收费 0-免费", required = true)
@NotNull @NotNull
private Integer isVip; private Integer isVip;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package io.github.xxyopen.novel.dto.resp; package io.github.xxyopen.novel.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -16,15 +17,18 @@ public class UserInfoRespDto {
/** /**
* 昵称 * 昵称
* */ * */
@Schema(description = "昵称")
private String nickName; private String nickName;
/** /**
* 用户头像 * 用户头像
* */ * */
@Schema(description = "用户头像")
private String userPhoto; private String userPhoto;
/** /**
* 用户性别 * 用户性别
* */ * */
@Schema(description = "用户性别")
private Integer userSex; private Integer userSex;
} }

View File

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

View File

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

View File

@ -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.entity.AuthorInfo;
import io.github.xxyopen.novel.dao.mapper.AuthorInfoMapper; import io.github.xxyopen.novel.dao.mapper.AuthorInfoMapper;
import io.github.xxyopen.novel.dto.AuthorInfoDto; import io.github.xxyopen.novel.dto.AuthorInfoDto;
import java.util.Objects;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Objects;
/** /**
* 作家信息 缓存管理类 * 作家信息 缓存管理类
* *
@ -29,24 +28,24 @@ public class AuthorInfoCacheManager {
* 查询作家信息,并放入缓存中 * 查询作家信息,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.AUTHOR_INFO_CACHE_NAME, unless = "#result == null") value = CacheConsts.AUTHOR_INFO_CACHE_NAME, unless = "#result == null")
public AuthorInfoDto getAuthor(Long userId) { public AuthorInfoDto getAuthor(Long userId) {
QueryWrapper<AuthorInfo> queryWrapper = new QueryWrapper<>(); QueryWrapper<AuthorInfo> queryWrapper = new QueryWrapper<>();
queryWrapper queryWrapper
.eq(DatabaseConsts.AuthorInfoTable.COLUMN_USER_ID, userId) .eq(DatabaseConsts.AuthorInfoTable.COLUMN_USER_ID, userId)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
AuthorInfo authorInfo = authorInfoMapper.selectOne(queryWrapper); AuthorInfo authorInfo = authorInfoMapper.selectOne(queryWrapper);
if (Objects.isNull(authorInfo)) { if (Objects.isNull(authorInfo)) {
return null; return null;
} }
return AuthorInfoDto.builder() return AuthorInfoDto.builder()
.id(authorInfo.getId()) .id(authorInfo.getId())
.penName(authorInfo.getPenName()) .penName(authorInfo.getPenName())
.status(authorInfo.getStatus()).build(); .status(authorInfo.getStatus()).build();
} }
@CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.AUTHOR_INFO_CACHE_NAME) value = CacheConsts.AUTHOR_INFO_CACHE_NAME)
public void evictAuthorCache() { public void evictAuthorCache() {
// 调用此方法自动清除作家信息的缓存 // 调用此方法自动清除作家信息的缓存
} }

View File

@ -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.entity.BookCategory;
import io.github.xxyopen.novel.dao.mapper.BookCategoryMapper; import io.github.xxyopen.novel.dao.mapper.BookCategoryMapper;
import io.github.xxyopen.novel.dto.resp.BookCategoryRespDto; import io.github.xxyopen.novel.dto.resp.BookCategoryRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 小说分类 缓存管理类 * 小说分类 缓存管理类
* *
@ -28,15 +27,15 @@ public class BookCategoryCacheManager {
* 根据作品方向查询小说分类列表,并放入缓存中 * 根据作品方向查询小说分类列表,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_CATEGORY_LIST_CACHE_NAME) value = CacheConsts.BOOK_CATEGORY_LIST_CACHE_NAME)
public List<BookCategoryRespDto> listCategory(Integer workDirection) { public List<BookCategoryRespDto> listCategory(Integer workDirection) {
QueryWrapper<BookCategory> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookCategory> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookCategoryTable.COLUMN_WORK_DIRECTION, workDirection); queryWrapper.eq(DatabaseConsts.BookCategoryTable.COLUMN_WORK_DIRECTION, workDirection);
return bookCategoryMapper.selectList(queryWrapper).stream().map(v -> return bookCategoryMapper.selectList(queryWrapper).stream().map(v ->
BookCategoryRespDto.builder() BookCategoryRespDto.builder()
.id(v.getId()) .id(v.getId())
.name(v.getName()) .name(v.getName())
.build()).toList(); .build()).toList();
} }
} }

View File

@ -24,17 +24,17 @@ public class BookChapterCacheManager {
* 查询小说章节信息,并放入缓存中 * 查询小说章节信息,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_CHAPTER_CACHE_NAME) value = CacheConsts.BOOK_CHAPTER_CACHE_NAME)
public BookChapterRespDto getChapter(Long chapterId) { public BookChapterRespDto getChapter(Long chapterId) {
BookChapter bookChapter = bookChapterMapper.selectById(chapterId); BookChapter bookChapter = bookChapterMapper.selectById(chapterId);
return BookChapterRespDto.builder() return BookChapterRespDto.builder()
.id(chapterId) .id(chapterId)
.bookId(bookChapter.getBookId()) .bookId(bookChapter.getBookId())
.chapterNum(bookChapter.getChapterNum()) .chapterNum(bookChapter.getChapterNum())
.chapterName(bookChapter.getChapterName()) .chapterName(bookChapter.getChapterName())
.chapterWordCount(bookChapter.getWordCount()) .chapterWordCount(bookChapter.getWordCount())
.chapterUpdateTime(bookChapter.getUpdateTime()) .chapterUpdateTime(bookChapter.getUpdateTime())
.build(); .build();
} }

View File

@ -25,11 +25,11 @@ public class BookContentCacheManager {
* 查询小说内容,并放入缓存中 * 查询小说内容,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.BOOK_CONTENT_CACHE_NAME) value = CacheConsts.BOOK_CONTENT_CACHE_NAME)
public String getBookContent(Long chapterId) { public String getBookContent(Long chapterId) {
QueryWrapper<BookContent> contentQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookContent> contentQueryWrapper = new QueryWrapper<>();
contentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId) contentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
BookContent bookContent = bookContentMapper.selectOne(contentQueryWrapper); BookContent bookContent = bookContentMapper.selectOne(contentQueryWrapper);
return bookContent.getContent(); return bookContent.getContent();
} }

View File

@ -8,14 +8,13 @@ import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookChapterMapper; import io.github.xxyopen.novel.dao.mapper.BookChapterMapper;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper; import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto; import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 小说信息 缓存管理类 * 小说信息 缓存管理类
* *
@ -34,7 +33,7 @@ public class BookInfoCacheManager {
* 从缓存中查询小说信息(先判断缓存中是否已存在,存在则直接从缓存中取,否则执行方法体中的逻辑后缓存结果) * 从缓存中查询小说信息(先判断缓存中是否已存在,存在则直接从缓存中取,否则执行方法体中的逻辑后缓存结果)
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_INFO_CACHE_NAME) value = CacheConsts.BOOK_INFO_CACHE_NAME)
public BookInfoRespDto getBookInfo(Long id) { public BookInfoRespDto getBookInfo(Long id) {
return cachePutBookInfo(id); return cachePutBookInfo(id);
} }
@ -43,38 +42,38 @@ public class BookInfoCacheManager {
* 缓存小说信息(不管缓存中是否存在都执行方法体中的逻辑,然后缓存起来) * 缓存小说信息(不管缓存中是否存在都执行方法体中的逻辑,然后缓存起来)
*/ */
@CachePut(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @CachePut(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_INFO_CACHE_NAME) value = CacheConsts.BOOK_INFO_CACHE_NAME)
public BookInfoRespDto cachePutBookInfo(Long id) { public BookInfoRespDto cachePutBookInfo(Long id) {
// 查询基础信息 // 查询基础信息
BookInfo bookInfo = bookInfoMapper.selectById(id); BookInfo bookInfo = bookInfoMapper.selectById(id);
// 查询首章ID // 查询首章ID
QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
queryWrapper queryWrapper
.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, id) .eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, id)
.orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
BookChapter firstBookChapter = bookChapterMapper.selectOne(queryWrapper); BookChapter firstBookChapter = bookChapterMapper.selectOne(queryWrapper);
// 组装响应对象 // 组装响应对象
return BookInfoRespDto.builder() return BookInfoRespDto.builder()
.id(bookInfo.getId()) .id(bookInfo.getId())
.bookName(bookInfo.getBookName()) .bookName(bookInfo.getBookName())
.bookDesc(bookInfo.getBookDesc()) .bookDesc(bookInfo.getBookDesc())
.bookStatus(bookInfo.getBookStatus()) .bookStatus(bookInfo.getBookStatus())
.authorId(bookInfo.getAuthorId()) .authorId(bookInfo.getAuthorId())
.authorName(bookInfo.getAuthorName()) .authorName(bookInfo.getAuthorName())
.categoryId(bookInfo.getCategoryId()) .categoryId(bookInfo.getCategoryId())
.categoryName(bookInfo.getCategoryName()) .categoryName(bookInfo.getCategoryName())
.commentCount(bookInfo.getCommentCount()) .commentCount(bookInfo.getCommentCount())
.firstChapterId(firstBookChapter.getId()) .firstChapterId(firstBookChapter.getId())
.lastChapterId(bookInfo.getLastChapterId()) .lastChapterId(bookInfo.getLastChapterId())
.picUrl(bookInfo.getPicUrl()) .picUrl(bookInfo.getPicUrl())
.visitCount(bookInfo.getVisitCount()) .visitCount(bookInfo.getVisitCount())
.wordCount(bookInfo.getWordCount()) .wordCount(bookInfo.getWordCount())
.build(); .build();
} }
@CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_INFO_CACHE_NAME) value = CacheConsts.BOOK_INFO_CACHE_NAME)
public void evictBookInfoCache(Long ignoredId) { public void evictBookInfoCache(Long ignoredId) {
// 调用此方法自动清除小说信息的缓存 // 调用此方法自动清除小说信息的缓存
} }
@ -83,13 +82,13 @@ public class BookInfoCacheManager {
* 查询每个类别下最新更新的 500 个小说ID列表并放入缓存中 1 个小时 * 查询每个类别下最新更新的 500 个小说ID列表并放入缓存中 1 个小时
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME) value = CacheConsts.LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME)
public List<Long> getLastUpdateIdList(Long categoryId) { public List<Long> getLastUpdateIdList(Long categoryId) {
QueryWrapper<BookInfo> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookTable.COLUMN_CATEGORY_ID, categoryId) queryWrapper.eq(DatabaseConsts.BookTable.COLUMN_CATEGORY_ID, categoryId)
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT,0) .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.orderByDesc(DatabaseConsts.BookTable.COLUMN_LAST_CHAPTER_UPDATE_TIME) .orderByDesc(DatabaseConsts.BookTable.COLUMN_LAST_CHAPTER_UPDATE_TIME)
.last(DatabaseConsts.SqlEnum.LIMIT_500.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_500.getSql());
return bookInfoMapper.selectList(queryWrapper).stream().map(BookInfo::getId).toList(); return bookInfoMapper.selectList(queryWrapper).stream().map(BookInfo::getId).toList();
} }

View File

@ -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.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper; import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.resp.BookRankRespDto; import io.github.xxyopen.novel.dto.resp.BookRankRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 小说排行榜 缓存管理类 * 小说排行榜 缓存管理类
* *
@ -28,7 +27,7 @@ public class BookRankCacheManager {
* 查询小说点击榜列表,并放入缓存中 * 查询小说点击榜列表,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.BOOK_VISIT_RANK_CACHE_NAME) value = CacheConsts.BOOK_VISIT_RANK_CACHE_NAME)
public List<BookRankRespDto> listVisitRankBooks() { public List<BookRankRespDto> listVisitRankBooks() {
QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>();
bookInfoQueryWrapper.orderByDesc(DatabaseConsts.BookTable.COLUMN_VISIT_COUNT); bookInfoQueryWrapper.orderByDesc(DatabaseConsts.BookTable.COLUMN_VISIT_COUNT);
@ -39,12 +38,12 @@ public class BookRankCacheManager {
* 查询小说新书榜列表,并放入缓存中 * 查询小说新书榜列表,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_NEWEST_RANK_CACHE_NAME) value = CacheConsts.BOOK_NEWEST_RANK_CACHE_NAME)
public List<BookRankRespDto> listNewestRankBooks() { public List<BookRankRespDto> listNewestRankBooks() {
QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>();
bookInfoQueryWrapper bookInfoQueryWrapper
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT,0) .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()); .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName());
return listRankBooks(bookInfoQueryWrapper); return listRankBooks(bookInfoQueryWrapper);
} }
@ -52,19 +51,19 @@ public class BookRankCacheManager {
* 查询小说更新榜列表,并放入缓存中 * 查询小说更新榜列表,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_UPDATE_RANK_CACHE_NAME) value = CacheConsts.BOOK_UPDATE_RANK_CACHE_NAME)
public List<BookRankRespDto> listUpdateRankBooks() { public List<BookRankRespDto> listUpdateRankBooks() {
QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>();
bookInfoQueryWrapper bookInfoQueryWrapper
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT,0) .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.orderByDesc(DatabaseConsts.CommonColumnEnum.UPDATE_TIME.getName()); .orderByDesc(DatabaseConsts.CommonColumnEnum.UPDATE_TIME.getName());
return listRankBooks(bookInfoQueryWrapper); return listRankBooks(bookInfoQueryWrapper);
} }
private List<BookRankRespDto> listRankBooks(QueryWrapper<BookInfo> bookInfoQueryWrapper) { private List<BookRankRespDto> listRankBooks(QueryWrapper<BookInfo> bookInfoQueryWrapper) {
bookInfoQueryWrapper bookInfoQueryWrapper
.gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT,0) .gt(DatabaseConsts.BookTable.COLUMN_WORD_COUNT, 0)
.last(DatabaseConsts.SqlEnum.LIMIT_30.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_30.getSql());
return bookInfoMapper.selectList(bookInfoQueryWrapper).stream().map(v -> { return bookInfoMapper.selectList(bookInfoQueryWrapper).stream().map(v -> {
BookRankRespDto respDto = new BookRankRespDto(); BookRankRespDto respDto = new BookRankRespDto();
respDto.setId(v.getId()); respDto.setId(v.getId());

View File

@ -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.entity.HomeFriendLink;
import io.github.xxyopen.novel.dao.mapper.HomeFriendLinkMapper; import io.github.xxyopen.novel.dao.mapper.HomeFriendLinkMapper;
import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto; import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 友情链接 缓存管理类 * 友情链接 缓存管理类
* *
@ -28,7 +27,7 @@ public class FriendLinkCacheManager {
* 友情链接列表查询,并放入缓存中 * 友情链接列表查询,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.HOME_FRIEND_LINK_CACHE_NAME) value = CacheConsts.HOME_FRIEND_LINK_CACHE_NAME)
public List<HomeFriendLinkRespDto> listFriendLinks() { public List<HomeFriendLinkRespDto> listFriendLinks() {
// 从友情链接表中查询出友情链接列表 // 从友情链接表中查询出友情链接列表
QueryWrapper<HomeFriendLink> queryWrapper = new QueryWrapper<>(); QueryWrapper<HomeFriendLink> queryWrapper = new QueryWrapper<>();

View File

@ -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.BookInfoMapper;
import io.github.xxyopen.novel.dao.mapper.HomeBookMapper; import io.github.xxyopen.novel.dao.mapper.HomeBookMapper;
import io.github.xxyopen.novel.dto.resp.HomeBookRespDto; 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.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
/** /**
* 首页推荐小说 缓存管理类 * 首页推荐小说 缓存管理类
@ -37,7 +36,7 @@ public class HomeBookCacheManager {
* 查询首页小说推荐,并放入缓存中 * 查询首页小说推荐,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.HOME_BOOK_CACHE_NAME) value = CacheConsts.HOME_BOOK_CACHE_NAME)
public List<HomeBookRespDto> listHomeBooks() { public List<HomeBookRespDto> listHomeBooks() {
// 从首页小说推荐表中查询出需要推荐的小说 // 从首页小说推荐表中查询出需要推荐的小说
QueryWrapper<HomeBook> queryWrapper = new QueryWrapper<>(); QueryWrapper<HomeBook> queryWrapper = new QueryWrapper<>();
@ -47,8 +46,8 @@ public class HomeBookCacheManager {
// 获取推荐小说ID列表 // 获取推荐小说ID列表
if (!CollectionUtils.isEmpty(homeBooks)) { if (!CollectionUtils.isEmpty(homeBooks)) {
List<Long> bookIds = homeBooks.stream() List<Long> bookIds = homeBooks.stream()
.map(HomeBook::getBookId) .map(HomeBook::getBookId)
.toList(); .toList();
// 根据小说ID列表查询相关的小说信息列表 // 根据小说ID列表查询相关的小说信息列表
QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>();
@ -56,9 +55,9 @@ public class HomeBookCacheManager {
List<BookInfo> bookInfos = bookInfoMapper.selectList(bookInfoQueryWrapper); List<BookInfo> bookInfos = bookInfoMapper.selectList(bookInfoQueryWrapper);
// 组装 HomeBookRespDto 列表数据并返回 // 组装 HomeBookRespDto 列表数据并返回
if(!CollectionUtils.isEmpty(bookInfos)){ if (!CollectionUtils.isEmpty(bookInfos)) {
Map<Long, BookInfo> bookInfoMap = bookInfos.stream() Map<Long, BookInfo> bookInfoMap = bookInfos.stream()
.collect(Collectors.toMap(BookInfo::getId, Function.identity())); .collect(Collectors.toMap(BookInfo::getId, Function.identity()));
return homeBooks.stream().map(v -> { return homeBooks.stream().map(v -> {
BookInfo bookInfo = bookInfoMap.get(v.getBookId()); BookInfo bookInfo = bookInfoMap.get(v.getBookId());
HomeBookRespDto bookRespDto = new HomeBookRespDto(); HomeBookRespDto bookRespDto = new HomeBookRespDto();

View File

@ -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.entity.NewsInfo;
import io.github.xxyopen.novel.dao.mapper.NewsInfoMapper; import io.github.xxyopen.novel.dao.mapper.NewsInfoMapper;
import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto; import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 新闻 缓存管理类 * 新闻 缓存管理类
* *
@ -28,20 +27,20 @@ public class NewsCacheManager {
* 最新新闻列表查询,并放入缓存中 * 最新新闻列表查询,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.LATEST_NEWS_CACHE_NAME) value = CacheConsts.LATEST_NEWS_CACHE_NAME)
public List<NewsInfoRespDto> listLatestNews() { public List<NewsInfoRespDto> listLatestNews() {
// 从新闻信息表中查询出最新发布的两条新闻 // 从新闻信息表中查询出最新发布的两条新闻
QueryWrapper<NewsInfo> queryWrapper = new QueryWrapper<>(); QueryWrapper<NewsInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()) 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() return newsInfoMapper.selectList(queryWrapper).stream().map(v -> NewsInfoRespDto.builder()
.id(v.getId()) .id(v.getId())
.categoryId(v.getCategoryId()) .categoryId(v.getCategoryId())
.categoryName(v.getCategoryName()) .categoryName(v.getCategoryName())
.title(v.getTitle()) .title(v.getTitle())
.sourceName(v.getSourceName()) .sourceName(v.getSourceName())
.updateTime(v.getUpdateTime()) .updateTime(v.getUpdateTime())
.build()).toList(); .build()).toList();
} }
} }

View File

@ -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.entity.UserInfo;
import io.github.xxyopen.novel.dao.mapper.UserInfoMapper; import io.github.xxyopen.novel.dao.mapper.UserInfoMapper;
import io.github.xxyopen.novel.dto.UserInfoDto; import io.github.xxyopen.novel.dto.UserInfoDto;
import java.util.Objects;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Objects;
/** /**
* 用户信息 缓存管理类 * 用户信息 缓存管理类
* *
@ -26,15 +25,15 @@ public class UserInfoCacheManager {
* 查询用户信息,并放入缓存中 * 查询用户信息,并放入缓存中
*/ */
@Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER, @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.USER_INFO_CACHE_NAME) value = CacheConsts.USER_INFO_CACHE_NAME)
public UserInfoDto getUser(Long userId) { public UserInfoDto getUser(Long userId) {
UserInfo userInfo = userInfoMapper.selectById(userId); UserInfo userInfo = userInfoMapper.selectById(userId);
if (Objects.isNull(userInfo)) { if (Objects.isNull(userInfo)) {
return null; return null;
} }
return UserInfoDto.builder() return UserInfoDto.builder()
.id(userInfo.getId()) .id(userInfo.getId())
.status(userInfo.getStatus()).build(); .status(userInfo.getStatus()).build();
} }

View File

@ -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.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.UserInfo; import io.github.xxyopen.novel.dao.entity.UserInfo;
import io.github.xxyopen.novel.dao.mapper.UserInfoMapper; import io.github.xxyopen.novel.dao.mapper.UserInfoMapper;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List;
/** /**
* 用户模块 DAO管理类 * 用户模块 DAO管理类
*
* @author xiongxiaoyang * @author xiongxiaoyang
* @date 2022/5/20 * @date 2022/5/20
*/ */
@ -22,12 +22,13 @@ public class UserDaoManager {
/** /**
* 根据用户ID集合批量查询用户信息列表 * 根据用户ID集合批量查询用户信息列表
*
* @param userIds 需要查询的用户ID集合 * @param userIds 需要查询的用户ID集合
* @return 满足条件的用户信息列表 * @return 满足条件的用户信息列表
* */ */
public List<UserInfo> listUsers(List<Long> userIds){ public List<UserInfo> listUsers(List<Long> userIds) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>(); QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.in(DatabaseConsts.CommonColumnEnum.ID.getName(),userIds); queryWrapper.in(DatabaseConsts.CommonColumnEnum.ID.getName(), userIds);
return userInfoMapper.selectList(queryWrapper); return userInfoMapper.selectList(queryWrapper);
} }

View File

@ -2,6 +2,7 @@ package io.github.xxyopen.novel.manager.mq;
import io.github.xxyopen.novel.core.common.constant.CommonConsts; import io.github.xxyopen.novel.core.common.constant.CommonConsts;
import io.github.xxyopen.novel.core.constant.AmqpConsts; import io.github.xxyopen.novel.core.constant.AmqpConsts;
import java.util.Objects;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -9,8 +10,6 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Objects;
/** /**
* AMQP 消息管理类 * AMQP 消息管理类
* *
@ -35,15 +34,17 @@ public class AmqpMsgManager {
} }
} }
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()) { if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { TransactionSynchronizationManager.registerSynchronization(
@Override new TransactionSynchronization() {
public void afterCommit() { @Override
amqpTemplate.convertAndSend(exchange, routingKey, message); public void afterCommit() {
} amqpTemplate.convertAndSend(exchange, routingKey, message);
}); }
});
return; return;
} }
amqpTemplate.convertAndSend(exchange, routingKey, message); amqpTemplate.convertAndSend(exchange, routingKey, message);

View File

@ -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.common.util.ImgVerifyCodeUtils;
import io.github.xxyopen.novel.core.constant.CacheConsts; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.Duration;
import java.util.Objects;
/** /**
* 验证码 管理类 * 验证码 管理类
* *
@ -31,7 +30,7 @@ public class VerifyCodeManager {
String verifyCode = ImgVerifyCodeUtils.getRandomVerifyCode(4); String verifyCode = ImgVerifyCodeUtils.getRandomVerifyCode(4);
String img = ImgVerifyCodeUtils.genVerifyCodeImg(verifyCode); String img = ImgVerifyCodeUtils.genVerifyCodeImg(verifyCode);
stringRedisTemplate.opsForValue().set(CacheConsts.IMG_VERIFY_CODE_CACHE_KEY + sessionId, stringRedisTemplate.opsForValue().set(CacheConsts.IMG_VERIFY_CODE_CACHE_KEY + sessionId,
verifyCode, Duration.ofMinutes(5)); verifyCode, Duration.ofMinutes(5));
return img; return img;
} }
@ -39,7 +38,8 @@ public class VerifyCodeManager {
* 校验图形验证码 * 校验图形验证码
*/ */
public boolean imgVerifyCodeOk(String sessionId, String verifyCode) { 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);
} }
/** /**

View File

@ -21,8 +21,9 @@ public interface AuthorService {
/** /**
* 查询作家状态 * 查询作家状态
*
* @param userId 用户ID * @param userId 用户ID
* @return 作家状态 * @return 作家状态
* */ */
RestResp<Integer> getStatus(Long userId); RestResp<Integer> getStatus(Long userId);
} }

View File

@ -3,13 +3,19 @@ package io.github.xxyopen.novel.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.auth.UserHolder;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum; import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.req.PageReqDto; 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.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp; import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.DatabaseConsts; import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.*; import io.github.xxyopen.novel.dao.entity.BookChapter;
import io.github.xxyopen.novel.dao.entity.BookComment;
import io.github.xxyopen.novel.dao.entity.BookContent;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.entity.UserInfo;
import io.github.xxyopen.novel.dao.mapper.BookChapterMapper; import io.github.xxyopen.novel.dao.mapper.BookChapterMapper;
import io.github.xxyopen.novel.dao.mapper.BookCommentMapper; import io.github.xxyopen.novel.dao.mapper.BookCommentMapper;
import io.github.xxyopen.novel.dao.mapper.BookContentMapper; import io.github.xxyopen.novel.dao.mapper.BookContentMapper;
@ -18,23 +24,39 @@ import io.github.xxyopen.novel.dto.AuthorInfoDto;
import io.github.xxyopen.novel.dto.req.BookAddReqDto; import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto; import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto; import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*; import io.github.xxyopen.novel.dto.resp.BookCategoryRespDto;
import io.github.xxyopen.novel.manager.cache.*; 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.manager.cache.AuthorInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.BookCategoryCacheManager;
import io.github.xxyopen.novel.manager.cache.BookChapterCacheManager;
import io.github.xxyopen.novel.manager.cache.BookContentCacheManager;
import io.github.xxyopen.novel.manager.cache.BookInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.BookRankCacheManager;
import io.github.xxyopen.novel.manager.dao.UserDaoManager; import io.github.xxyopen.novel.manager.dao.UserDaoManager;
import io.github.xxyopen.novel.manager.mq.AmqpMsgManager; import io.github.xxyopen.novel.manager.mq.AmqpMsgManager;
import io.github.xxyopen.novel.service.BookService; import io.github.xxyopen.novel.service.BookService;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/** /**
* 小说模块 服务实现类 * 小说模块 服务实现类
* *
@ -98,7 +120,8 @@ public class BookServiceImpl implements BookService {
BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(bookId); BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(bookId);
// 查询最新章节信息 // 查询最新章节信息
BookChapterRespDto bookChapter = bookChapterCacheManager.getChapter(bookInfo.getLastChapterId()); BookChapterRespDto bookChapter = bookChapterCacheManager.getChapter(
bookInfo.getLastChapterId());
// 查询章节内容 // 查询章节内容
String content = bookContentCacheManager.getBookContent(bookInfo.getLastChapterId()); String content = bookContentCacheManager.getBookContent(bookInfo.getLastChapterId());
@ -110,14 +133,15 @@ public class BookServiceImpl implements BookService {
// 组装数据并返回 // 组装数据并返回
return RestResp.ok(BookChapterAboutRespDto.builder() return RestResp.ok(BookChapterAboutRespDto.builder()
.chapterInfo(bookChapter) .chapterInfo(bookChapter)
.chapterTotal(chapterTotal) .chapterTotal(chapterTotal)
.contentSummary(content.substring(0, 30)) .contentSummary(content.substring(0, 30))
.build()); .build());
} }
@Override @Override
public RestResp<List<BookInfoRespDto>> listRecBooks(Long bookId) throws NoSuchAlgorithmException { public RestResp<List<BookInfoRespDto>> listRecBooks(Long bookId)
throws NoSuchAlgorithmException {
Long categoryId = bookInfoCacheManager.getBookInfo(bookId).getCategoryId(); Long categoryId = bookInfoCacheManager.getBookInfo(bookId).getCategoryId();
List<Long> lastUpdateIdList = bookInfoCacheManager.getLastUpdateIdList(categoryId); List<Long> lastUpdateIdList = bookInfoCacheManager.getLastUpdateIdList(categoryId);
List<BookInfoRespDto> respDtoList = new ArrayList<>(); List<BookInfoRespDto> respDtoList = new ArrayList<>();
@ -153,13 +177,13 @@ public class BookServiceImpl implements BookService {
// 查询上一章信息并返回章节ID // 查询上一章信息并返回章节ID
QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId) queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId)
.lt(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM, chapterNum) .lt(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM, chapterNum)
.orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
return RestResp.ok( return RestResp.ok(
Optional.ofNullable(bookChapterMapper.selectOne(queryWrapper)) Optional.ofNullable(bookChapterMapper.selectOne(queryWrapper))
.map(BookChapter::getId) .map(BookChapter::getId)
.orElse(null) .orElse(null)
); );
} }
@ -173,13 +197,13 @@ public class BookServiceImpl implements BookService {
// 查询下一章信息并返回章节ID // 查询下一章信息并返回章节ID
QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId) queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId)
.gt(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM, chapterNum) .gt(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM, chapterNum)
.orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
return RestResp.ok( return RestResp.ok(
Optional.ofNullable(bookChapterMapper.selectOne(queryWrapper)) Optional.ofNullable(bookChapterMapper.selectOne(queryWrapper))
.map(BookChapter::getId) .map(BookChapter::getId)
.orElse(null) .orElse(null)
); );
} }
@ -187,8 +211,9 @@ public class BookServiceImpl implements BookService {
public RestResp<List<BookChapterRespDto>> listChapters(Long bookId) { public RestResp<List<BookChapterRespDto>> listChapters(Long bookId) {
QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId) queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId)
.orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM); .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM);
return RestResp.ok(bookChapterMapper.selectList(queryWrapper).stream().map(v -> BookChapterRespDto.builder() return RestResp.ok(bookChapterMapper.selectList(queryWrapper).stream()
.map(v -> BookChapterRespDto.builder()
.id(v.getId()) .id(v.getId())
.chapterName(v.getChapterName()) .chapterName(v.getChapterName())
.isVip(v.getIsVip()) .isVip(v.getIsVip())
@ -200,12 +225,14 @@ public class BookServiceImpl implements BookService {
return RestResp.ok(bookCategoryCacheManager.listCategory(workDirection)); return RestResp.ok(bookCategoryCacheManager.listCategory(workDirection));
} }
@Lock(prefix = "userComment")
@Override @Override
public RestResp<Void> saveComment(UserCommentReqDto dto) { public RestResp<Void> saveComment(
@Key(expr = "#{userId + '::' + bookId}") UserCommentReqDto dto) {
// 校验用户是否已发表评论 // 校验用户是否已发表评论
QueryWrapper<BookComment> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookComment> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, dto.getUserId()) queryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, dto.getUserId())
.eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, dto.getBookId()); .eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, dto.getBookId());
if (bookCommentMapper.selectCount(queryWrapper) > 0) { if (bookCommentMapper.selectCount(queryWrapper) > 0) {
// 用户已发表评论 // 用户已发表评论
return RestResp.fail(ErrorCodeEnum.USER_COMMENTED); return RestResp.fail(ErrorCodeEnum.USER_COMMENTED);
@ -226,28 +253,30 @@ public class BookServiceImpl implements BookService {
QueryWrapper<BookComment> commentCountQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookComment> commentCountQueryWrapper = new QueryWrapper<>();
commentCountQueryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, bookId); commentCountQueryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, bookId);
Long commentTotal = bookCommentMapper.selectCount(commentCountQueryWrapper); Long commentTotal = bookCommentMapper.selectCount(commentCountQueryWrapper);
BookCommentRespDto bookCommentRespDto = BookCommentRespDto.builder().commentTotal(commentTotal).build(); BookCommentRespDto bookCommentRespDto = BookCommentRespDto.builder()
.commentTotal(commentTotal).build();
if (commentTotal > 0) { if (commentTotal > 0) {
// 查询最新的评论列表 // 查询最新的评论列表
QueryWrapper<BookComment> commentQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookComment> commentQueryWrapper = new QueryWrapper<>();
commentQueryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, bookId) commentQueryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_BOOK_ID, bookId)
.orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()) .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName())
.last(DatabaseConsts.SqlEnum.LIMIT_5.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_5.getSql());
List<BookComment> bookComments = bookCommentMapper.selectList(commentQueryWrapper); List<BookComment> bookComments = bookCommentMapper.selectList(commentQueryWrapper);
// 查询评论用户信息,并设置需要返回的评论用户名 // 查询评论用户信息,并设置需要返回的评论用户名
List<Long> userIds = bookComments.stream().map(BookComment::getUserId).toList(); List<Long> userIds = bookComments.stream().map(BookComment::getUserId).toList();
List<UserInfo> userInfos = userDaoManager.listUsers(userIds); List<UserInfo> userInfos = userDaoManager.listUsers(userIds);
Map<Long, UserInfo> userInfoMap = userInfos.stream().collect(Collectors.toMap(UserInfo::getId, Function.identity())); Map<Long, UserInfo> userInfoMap = userInfos.stream()
.collect(Collectors.toMap(UserInfo::getId, Function.identity()));
List<BookCommentRespDto.CommentInfo> commentInfos = bookComments.stream() List<BookCommentRespDto.CommentInfo> commentInfos = bookComments.stream()
.map(v -> BookCommentRespDto.CommentInfo.builder() .map(v -> BookCommentRespDto.CommentInfo.builder()
.id(v.getId()) .id(v.getId())
.commentUserId(v.getUserId()) .commentUserId(v.getUserId())
.commentUser(userInfoMap.get(v.getUserId()).getUsername()) .commentUser(userInfoMap.get(v.getUserId()).getUsername())
.commentUserPhoto(userInfoMap.get(v.getUserId()).getUserPhoto()) .commentUserPhoto(userInfoMap.get(v.getUserId()).getUserPhoto())
.commentContent(v.getCommentContent()) .commentContent(v.getCommentContent())
.commentTime(v.getCreateTime()).build()).toList(); .commentTime(v.getCreateTime()).build()).toList();
bookCommentRespDto.setComments(commentInfos); bookCommentRespDto.setComments(commentInfos);
} else { } else {
bookCommentRespDto.setComments(Collections.emptyList()); bookCommentRespDto.setComments(Collections.emptyList());
@ -259,7 +288,7 @@ public class BookServiceImpl implements BookService {
public RestResp<Void> deleteComment(Long userId, Long commentId) { public RestResp<Void> deleteComment(Long userId, Long commentId) {
QueryWrapper<BookComment> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookComment> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.CommonColumnEnum.ID.getName(), commentId) queryWrapper.eq(DatabaseConsts.CommonColumnEnum.ID.getName(), commentId)
.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, userId); .eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, userId);
bookCommentMapper.delete(queryWrapper); bookCommentMapper.delete(queryWrapper);
return RestResp.ok(); return RestResp.ok();
} }
@ -268,7 +297,7 @@ public class BookServiceImpl implements BookService {
public RestResp<Void> updateComment(Long userId, Long id, String content) { public RestResp<Void> updateComment(Long userId, Long id, String content) {
QueryWrapper<BookComment> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookComment> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.CommonColumnEnum.ID.getName(), id) 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 bookComment = new BookComment();
bookComment.setCommentContent(content); bookComment.setCommentContent(content);
bookCommentMapper.update(bookComment, queryWrapper); bookCommentMapper.update(bookComment, queryWrapper);
@ -317,8 +346,8 @@ public class BookServiceImpl implements BookService {
int chapterNum = 0; int chapterNum = 0;
QueryWrapper<BookChapter> chapterQueryWrapper = new QueryWrapper<>(); QueryWrapper<BookChapter> chapterQueryWrapper = new QueryWrapper<>();
chapterQueryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, dto.getBookId()) chapterQueryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, dto.getBookId())
.orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM) .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql()); .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
BookChapter bookChapter = bookChapterMapper.selectOne(chapterQueryWrapper); BookChapter bookChapter = bookChapterMapper.selectOne(chapterQueryWrapper);
if (Objects.nonNull(bookChapter)) { if (Objects.nonNull(bookChapter)) {
chapterNum = bookChapter.getChapterNum() + 1; chapterNum = bookChapter.getChapterNum() + 1;
@ -366,18 +395,18 @@ public class BookServiceImpl implements BookService {
page.setSize(dto.getPageSize()); page.setSize(dto.getPageSize());
QueryWrapper<BookInfo> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookTable.AUTHOR_ID, UserHolder.getAuthorId()) queryWrapper.eq(DatabaseConsts.BookTable.AUTHOR_ID, UserHolder.getAuthorId())
.orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName()); .orderByDesc(DatabaseConsts.CommonColumnEnum.CREATE_TIME.getName());
IPage<BookInfo> bookInfoPage = bookInfoMapper.selectPage(page, queryWrapper); IPage<BookInfo> bookInfoPage = bookInfoMapper.selectPage(page, queryWrapper);
return RestResp.ok(PageRespDto.of(dto.getPageNum(), dto.getPageSize(), page.getTotal(), return RestResp.ok(PageRespDto.of(dto.getPageNum(), dto.getPageSize(), page.getTotal(),
bookInfoPage.getRecords().stream().map(v -> BookInfoRespDto.builder() bookInfoPage.getRecords().stream().map(v -> BookInfoRespDto.builder()
.id(v.getId()) .id(v.getId())
.bookName(v.getBookName()) .bookName(v.getBookName())
.picUrl(v.getPicUrl()) .picUrl(v.getPicUrl())
.categoryName(v.getCategoryName()) .categoryName(v.getCategoryName())
.wordCount(v.getWordCount()) .wordCount(v.getWordCount())
.visitCount(v.getVisitCount()) .visitCount(v.getVisitCount())
.updateTime(v.getUpdateTime()) .updateTime(v.getUpdateTime())
.build()).toList())); .build()).toList()));
} }
@Override @Override
@ -387,15 +416,15 @@ public class BookServiceImpl implements BookService {
page.setSize(dto.getPageSize()); page.setSize(dto.getPageSize());
QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>(); QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId) queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId)
.orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM); .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM);
IPage<BookChapter> bookChapterPage = bookChapterMapper.selectPage(page, queryWrapper); IPage<BookChapter> bookChapterPage = bookChapterMapper.selectPage(page, queryWrapper);
return RestResp.ok(PageRespDto.of(dto.getPageNum(), dto.getPageSize(), page.getTotal(), return RestResp.ok(PageRespDto.of(dto.getPageNum(), dto.getPageSize(), page.getTotal(),
bookChapterPage.getRecords().stream().map(v -> BookChapterRespDto.builder() bookChapterPage.getRecords().stream().map(v -> BookChapterRespDto.builder()
.id(v.getId()) .id(v.getId())
.chapterName(v.getChapterName()) .chapterName(v.getChapterName())
.chapterUpdateTime(v.getUpdateTime()) .chapterUpdateTime(v.getUpdateTime())
.isVip(v.getIsVip()) .isVip(v.getIsVip())
.build()).toList())); .build()).toList()));
} }
@Override @Override
@ -412,9 +441,9 @@ public class BookServiceImpl implements BookService {
// 组装数据并返回 // 组装数据并返回
return RestResp.ok(BookContentAboutRespDto.builder() return RestResp.ok(BookContentAboutRespDto.builder()
.bookInfo(bookInfo) .bookInfo(bookInfo)
.chapterInfo(bookChapter) .chapterInfo(bookChapter)
.bookContent(content) .bookContent(content)
.build()); .build());
} }
} }

View File

@ -8,13 +8,12 @@ import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto; import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto; import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.SearchService; import io.github.xxyopen.novel.service.SearchService;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* 数据库搜索 服务实现类 * 数据库搜索 服务实现类
* *
@ -35,17 +34,18 @@ public class DbSearchServiceImpl implements SearchService {
page.setCurrent(condition.getPageNum()); page.setCurrent(condition.getPageNum());
page.setSize(condition.getPageSize()); page.setSize(condition.getPageSize());
List<BookInfo> bookInfos = bookInfoMapper.searchBooks(page, condition); List<BookInfo> bookInfos = bookInfoMapper.searchBooks(page, condition);
return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), page.getTotal(), return RestResp.ok(
PageRespDto.of(condition.getPageNum(), condition.getPageSize(), page.getTotal(),
bookInfos.stream().map(v -> BookInfoRespDto.builder() bookInfos.stream().map(v -> BookInfoRespDto.builder()
.id(v.getId()) .id(v.getId())
.bookName(v.getBookName()) .bookName(v.getBookName())
.categoryId(v.getCategoryId()) .categoryId(v.getCategoryId())
.categoryName(v.getCategoryName()) .categoryName(v.getCategoryName())
.authorId(v.getAuthorId()) .authorId(v.getAuthorId())
.authorName(v.getAuthorName()) .authorName(v.getAuthorName())
.wordCount(v.getWordCount()) .wordCount(v.getWordCount())
.lastChapterName(v.getLastChapterName()) .lastChapterName(v.getLastChapterName())
.build()).toList())); .build()).toList()));
} }
} }

View File

@ -19,16 +19,15 @@ import io.github.xxyopen.novel.dto.es.EsBookDto;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto; import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto; import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.SearchService; import io.github.xxyopen.novel.service.SearchService;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** /**
* Elasticsearch 搜索 服务实现类 * Elasticsearch 搜索 服务实现类
* *
@ -49,79 +48,84 @@ public class EsSearchServiceImpl implements SearchService {
SearchResponse<EsBookDto> response = esClient.search(s -> { SearchResponse<EsBookDto> response = esClient.search(s -> {
SearchRequest.Builder searchBuilder = s.index(EsConsts.BookIndex.INDEX_NAME); SearchRequest.Builder searchBuilder = s.index(EsConsts.BookIndex.INDEX_NAME);
// 构建检索条件 // 构建检索条件
buildSearchCondition(condition, searchBuilder); buildSearchCondition(condition, searchBuilder);
// 排序 // 排序
if (!StringUtils.isBlank(condition.getSort())) { if (!StringUtils.isBlank(condition.getSort())) {
searchBuilder.sort(o -> o.field(f -> f searchBuilder.sort(o -> o.field(f -> f
.field(StringUtils.underlineToCamel(condition.getSort().split(" ")[0])) .field(StringUtils.underlineToCamel(condition.getSort().split(" ")[0]))
.order(SortOrder.Desc)) .order(SortOrder.Desc))
); );
} }
// 分页 // 分页
searchBuilder.from((condition.getPageNum() - 1) * condition.getPageSize()) searchBuilder.from((condition.getPageNum() - 1) * condition.getPageSize())
.size(condition.getPageSize()); .size(condition.getPageSize());
// 设置高亮显示 // 设置高亮显示
searchBuilder.highlight(h -> h.fields(EsConsts.BookIndex.FIELD_BOOK_NAME, searchBuilder.highlight(h -> h.fields(EsConsts.BookIndex.FIELD_BOOK_NAME,
t -> t.preTags("<em style='color:red'>").postTags("</em>")) t -> t.preTags("<em style='color:red'>").postTags("</em>"))
.fields(EsConsts.BookIndex.FIELD_AUTHOR_NAME, .fields(EsConsts.BookIndex.FIELD_AUTHOR_NAME,
t -> t.preTags("<em style='color:red'>").postTags("</em>"))); t -> t.preTags("<em style='color:red'>").postTags("</em>")));
return searchBuilder; return searchBuilder;
}, },
EsBookDto.class EsBookDto.class
); );
TotalHits total = response.hits().total(); TotalHits total = response.hits().total();
List<BookInfoRespDto> list = new ArrayList<>(); List<BookInfoRespDto> list = new ArrayList<>();
List<Hit<EsBookDto>> hits = response.hits().hits(); List<Hit<EsBookDto>> hits = response.hits().hits();
for (Hit<EsBookDto> hit : hits) { // 类型推断 var 非常适合 for 循环JDK 10 引入JDK 11 改进
for (var hit : hits) {
EsBookDto book = hit.source(); EsBookDto book = hit.source();
assert book != null; assert book != null;
if (!CollectionUtils.isEmpty(hit.highlight().get(EsConsts.BookIndex.FIELD_BOOK_NAME))) { if (!CollectionUtils.isEmpty(hit.highlight().get(EsConsts.BookIndex.FIELD_BOOK_NAME))) {
book.setBookName(hit.highlight().get(EsConsts.BookIndex.FIELD_BOOK_NAME).get(0)); book.setBookName(hit.highlight().get(EsConsts.BookIndex.FIELD_BOOK_NAME).get(0));
} }
if (!CollectionUtils.isEmpty(hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME))) { if (!CollectionUtils.isEmpty(
book.setAuthorName(hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME).get(0)); hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME))) {
book.setAuthorName(
hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME).get(0));
} }
list.add(BookInfoRespDto.builder() list.add(BookInfoRespDto.builder()
.id(book.getId()) .id(book.getId())
.bookName(book.getBookName()) .bookName(book.getBookName())
.categoryId(book.getCategoryId()) .categoryId(book.getCategoryId())
.categoryName(book.getCategoryName()) .categoryName(book.getCategoryName())
.authorId(book.getAuthorId()) .authorId(book.getAuthorId())
.authorName(book.getAuthorName()) .authorName(book.getAuthorName())
.wordCount(book.getWordCount()) .wordCount(book.getWordCount())
.lastChapterName(book.getLastChapterName()) .lastChapterName(book.getLastChapterName())
.build()); .build());
} }
assert total != null; 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 -> { BoolQuery boolQuery = BoolQuery.of(b -> {
// 只查有字数的小说 // 只查有字数的小说
b.must(RangeQuery.of(m -> m b.must(RangeQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_WORD_COUNT) .field(EsConsts.BookIndex.FIELD_WORD_COUNT)
.gt(JsonData.of(0)) .gt(JsonData.of(0))
)._toQuery()); )._toQuery());
if (!StringUtils.isBlank(condition.getKeyword())) { if (!StringUtils.isBlank(condition.getKeyword())) {
// 关键词匹配 // 关键词匹配
b.must((q -> q.multiMatch(t -> t b.must((q -> q.multiMatch(t -> t
.fields(EsConsts.BookIndex.FIELD_BOOK_NAME + "^2", .fields(EsConsts.BookIndex.FIELD_BOOK_NAME + "^2",
EsConsts.BookIndex.FIELD_AUTHOR_NAME + "^1.8", EsConsts.BookIndex.FIELD_AUTHOR_NAME + "^1.8",
EsConsts.BookIndex.FIELD_BOOK_DESC + "^0.1") EsConsts.BookIndex.FIELD_BOOK_DESC + "^0.1")
.query(condition.getKeyword()) .query(condition.getKeyword())
) )
)); ));
} }
@ -129,37 +133,37 @@ public class EsSearchServiceImpl implements SearchService {
// 精确查询 // 精确查询
if (Objects.nonNull(condition.getWorkDirection())) { if (Objects.nonNull(condition.getWorkDirection())) {
b.must(TermQuery.of(m -> m b.must(TermQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_WORK_DIRECTION) .field(EsConsts.BookIndex.FIELD_WORK_DIRECTION)
.value(condition.getWorkDirection()) .value(condition.getWorkDirection())
)._toQuery()); )._toQuery());
} }
if (Objects.nonNull(condition.getCategoryId())) { if (Objects.nonNull(condition.getCategoryId())) {
b.must(TermQuery.of(m -> m b.must(TermQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_CATEGORY_ID) .field(EsConsts.BookIndex.FIELD_CATEGORY_ID)
.value(condition.getCategoryId()) .value(condition.getCategoryId())
)._toQuery()); )._toQuery());
} }
// 范围查询 // 范围查询
if (Objects.nonNull(condition.getWordCountMin())) { if (Objects.nonNull(condition.getWordCountMin())) {
b.must(RangeQuery.of(m -> m b.must(RangeQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_WORD_COUNT) .field(EsConsts.BookIndex.FIELD_WORD_COUNT)
.gte(JsonData.of(condition.getWordCountMin())) .gte(JsonData.of(condition.getWordCountMin()))
)._toQuery()); )._toQuery());
} }
if (Objects.nonNull(condition.getWordCountMax())) { if (Objects.nonNull(condition.getWordCountMax())) {
b.must(RangeQuery.of(m -> m b.must(RangeQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_WORD_COUNT) .field(EsConsts.BookIndex.FIELD_WORD_COUNT)
.lt(JsonData.of(condition.getWordCountMax())) .lt(JsonData.of(condition.getWordCountMax()))
)._toQuery()); )._toQuery());
} }
if (Objects.nonNull(condition.getUpdateTimeMin())) { if (Objects.nonNull(condition.getUpdateTimeMin())) {
b.must(RangeQuery.of(m -> m b.must(RangeQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_LAST_CHAPTER_UPDATE_TIME) .field(EsConsts.BookIndex.FIELD_LAST_CHAPTER_UPDATE_TIME)
.gte(JsonData.of(condition.getUpdateTimeMin().getTime())) .gte(JsonData.of(condition.getUpdateTimeMin().getTime()))
)._toQuery()); )._toQuery());
} }

View File

@ -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.dto.resp.NewsInfoRespDto;
import io.github.xxyopen.novel.manager.cache.NewsCacheManager; import io.github.xxyopen.novel.manager.cache.NewsCacheManager;
import io.github.xxyopen.novel.service.NewsService; import io.github.xxyopen.novel.service.NewsService;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* 新闻模块 服务实现类 * 新闻模块 服务实现类
* *
@ -41,13 +40,13 @@ public class NewsServiceImpl implements NewsService {
NewsInfo newsInfo = newsInfoMapper.selectById(id); NewsInfo newsInfo = newsInfoMapper.selectById(id);
QueryWrapper<NewsContent> queryWrapper = new QueryWrapper<>(); QueryWrapper<NewsContent> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.NewsContentTable.COLUMN_NEWS_ID, id) 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); NewsContent newsContent = newsContentMapper.selectOne(queryWrapper);
return RestResp.ok(NewsInfoRespDto.builder() return RestResp.ok(NewsInfoRespDto.builder()
.title(newsInfo.getTitle()) .title(newsInfo.getTitle())
.sourceName(newsInfo.getSourceName()) .sourceName(newsInfo.getSourceName())
.updateTime(newsInfo.getUpdateTime()) .updateTime(newsInfo.getUpdateTime())
.content(newsContent.getContent()) .content(newsContent.getContent())
.build()); .build());
} }
} }

View File

@ -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.dto.resp.ImgVerifyCodeRespDto;
import io.github.xxyopen.novel.manager.redis.VerifyCodeManager; import io.github.xxyopen.novel.manager.redis.VerifyCodeManager;
import io.github.xxyopen.novel.service.ResourceService; 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.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Objects; 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<ImgVerifyCodeRespDto> getImgVerifyCode() throws IOException { public RestResp<ImgVerifyCodeRespDto> getImgVerifyCode() throws IOException {
String sessionId = IdWorker.get32UUID(); String sessionId = IdWorker.get32UUID();
return RestResp.ok(ImgVerifyCodeRespDto.builder() return RestResp.ok(ImgVerifyCodeRespDto.builder()
.sessionId(sessionId) .sessionId(sessionId)
.img(verifyCodeManager.genImgVerifyCode(sessionId)) .img(verifyCodeManager.genImgVerifyCode(sessionId))
.build()); .build());
} }
@SneakyThrows @SneakyThrows
@ -53,10 +52,10 @@ public class ResourceServiceImpl implements ResourceService {
public RestResp<String> uploadImage(MultipartFile file) { public RestResp<String> uploadImage(MultipartFile file) {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
String savePath = String savePath =
SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY
+ now.format(DateTimeFormatter.ofPattern("yyyy")) + File.separator + now.format(DateTimeFormatter.ofPattern("yyyy")) + File.separator
+ now.format(DateTimeFormatter.ofPattern("MM")) + File.separator + now.format(DateTimeFormatter.ofPattern("MM")) + File.separator
+ now.format(DateTimeFormatter.ofPattern("dd")); + now.format(DateTimeFormatter.ofPattern("dd"));
String oriName = file.getOriginalFilename(); String oriName = file.getOriginalFilename();
assert oriName != null; assert oriName != null;
String saveFileName = IdWorker.get32UUID() + oriName.substring(oriName.lastIndexOf(".")); String saveFileName = IdWorker.get32UUID() + oriName.substring(oriName.lastIndexOf("."));

View File

@ -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.dto.resp.UserRegisterRespDto;
import io.github.xxyopen.novel.manager.redis.VerifyCodeManager; import io.github.xxyopen.novel.manager.redis.VerifyCodeManager;
import io.github.xxyopen.novel.service.UserService; 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.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects; 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<UserInfo> queryWrapper = new QueryWrapper<>(); QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.UserInfoTable.COLUMN_USERNAME, dto.getUsername()) 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) { if (userInfoMapper.selectCount(queryWrapper) > 0) {
// 手机号已注册 // 手机号已注册
throw new BusinessException(ErrorCodeEnum.USER_NAME_EXIST); throw new BusinessException(ErrorCodeEnum.USER_NAME_EXIST);
@ -69,7 +68,8 @@ public class UserServiceImpl implements UserService {
// 注册成功,保存用户信息 // 注册成功,保存用户信息
UserInfo userInfo = new UserInfo(); 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.setUsername(dto.getUsername());
userInfo.setNickName(dto.getUsername()); userInfo.setNickName(dto.getUsername());
userInfo.setCreateTime(LocalDateTime.now()); userInfo.setCreateTime(LocalDateTime.now());
@ -82,10 +82,10 @@ public class UserServiceImpl implements UserService {
// 生成JWT 并返回 // 生成JWT 并返回
return RestResp.ok( return RestResp.ok(
UserRegisterRespDto.builder() UserRegisterRespDto.builder()
.token(jwtUtils.generateToken(userInfo.getId(), SystemConfigConsts.NOVEL_FRONT_KEY)) .token(jwtUtils.generateToken(userInfo.getId(), SystemConfigConsts.NOVEL_FRONT_KEY))
.uid(userInfo.getId()) .uid(userInfo.getId())
.build() .build()
); );
} }
@ -95,7 +95,7 @@ public class UserServiceImpl implements UserService {
// 查询用户信息 // 查询用户信息
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>(); QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.UserInfoTable.COLUMN_USERNAME, dto.getUsername()) 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); UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
if (Objects.isNull(userInfo)) { if (Objects.isNull(userInfo)) {
// 用户不存在 // 用户不存在
@ -104,16 +104,16 @@ public class UserServiceImpl implements UserService {
// 判断密码是否正确 // 判断密码是否正确
if (!Objects.equals(userInfo.getPassword() 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); throw new BusinessException(ErrorCodeEnum.USER_PASSWORD_ERROR);
} }
// 登录成功生成JWT并返回 // 登录成功生成JWT并返回
return RestResp.ok(UserLoginRespDto.builder() return RestResp.ok(UserLoginRespDto.builder()
.token(jwtUtils.generateToken(userInfo.getId(), SystemConfigConsts.NOVEL_FRONT_KEY)) .token(jwtUtils.generateToken(userInfo.getId(), SystemConfigConsts.NOVEL_FRONT_KEY))
.uid(userInfo.getId()) .uid(userInfo.getId())
.nickName(userInfo.getNickName()).build()); .nickName(userInfo.getNickName()).build());
} }
@Override @Override
@ -142,7 +142,7 @@ public class UserServiceImpl implements UserService {
public RestResp<Void> deleteFeedback(Long userId, Long id) { public RestResp<Void> deleteFeedback(Long userId, Long id) {
QueryWrapper<UserFeedback> queryWrapper = new QueryWrapper<>(); QueryWrapper<UserFeedback> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.CommonColumnEnum.ID.getName(), id) queryWrapper.eq(DatabaseConsts.CommonColumnEnum.ID.getName(), id)
.eq(DatabaseConsts.UserFeedBackTable.COLUMN_USER_ID, userId); .eq(DatabaseConsts.UserFeedBackTable.COLUMN_USER_ID, userId);
userFeedbackMapper.delete(queryWrapper); userFeedbackMapper.delete(queryWrapper);
return RestResp.ok(); return RestResp.ok();
} }
@ -151,11 +151,11 @@ public class UserServiceImpl implements UserService {
public RestResp<Integer> getBookshelfStatus(Long userId, String bookId) { public RestResp<Integer> getBookshelfStatus(Long userId, String bookId) {
QueryWrapper<UserBookshelf> queryWrapper = new QueryWrapper<>(); QueryWrapper<UserBookshelf> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.UserBookshelfTable.COLUMN_USER_ID, userId) 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( return RestResp.ok(
userBookshelfMapper.selectCount(queryWrapper) > 0 userBookshelfMapper.selectCount(queryWrapper) > 0
? CommonConsts.YES ? CommonConsts.YES
: CommonConsts.NO : CommonConsts.NO
); );
} }
@ -163,9 +163,9 @@ public class UserServiceImpl implements UserService {
public RestResp<UserInfoRespDto> getUserInfo(Long userId) { public RestResp<UserInfoRespDto> getUserInfo(Long userId) {
UserInfo userInfo = userInfoMapper.selectById(userId); UserInfo userInfo = userInfoMapper.selectById(userId);
return RestResp.ok(UserInfoRespDto.builder() return RestResp.ok(UserInfoRespDto.builder()
.nickName(userInfo.getNickName()) .nickName(userInfo.getNickName())
.userSex(userInfo.getUserSex()) .userSex(userInfo.getUserSex())
.userPhoto(userInfo.getUserPhoto()) .userPhoto(userInfo.getUserPhoto())
.build()); .build());
} }
} }

View File

@ -1,7 +1,6 @@
spring: spring:
profiles: profiles:
active: dev active: dev
# 将所有数字转为 String 类型返回,避免前端数据精度丢失的问题 # 将所有数字转为 String 类型返回,避免前端数据精度丢失的问题
jackson: jackson:
generator: generator:
@ -10,6 +9,8 @@ spring:
# 上传文件最大大小 # 上传文件最大大小
multipart: multipart:
max-file-size: 5MB max-file-size: 5MB
application:
name: novel
server: server:
port: 8888 port: 8888
@ -129,6 +130,66 @@ xxl:
accessToken: 123 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
--- ---
spring: spring:
config: config:

View File

@ -53,26 +53,26 @@
<!-- ROOT 日志级别 --> <!-- ROOT 日志级别 -->
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root> </root>
<!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 --> <!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
<!-- com.maijinjie.springboot 为根包也就是只要是发生在这个根包下面的所有日志操作行为的权限都是DEBUG --> <!-- com.maijinjie.springboot 为根包也就是只要是发生在这个根包下面的所有日志操作行为的权限都是DEBUG -->
<!-- 级别依次为【从高到低】FATAL > ERROR > WARN > INFO > DEBUG > TRACE --> <!-- 级别依次为【从高到低】FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
<logger name="io.github.xxyopen" level="DEBUG" additivity="false"> <logger name="io.github.xxyopen" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger> </logger>
</springProfile> </springProfile>
<springProfile name="prod"> <springProfile name="prod">
<!-- ROOT 日志级别 --> <!-- ROOT 日志级别 -->
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/> <appender-ref ref="FILE"/>
</root> </root>
<!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 --> <!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
<!-- com.maijinjie.springboot 为根包也就是只要是发生在这个根包下面的所有日志操作行为的权限都是DEBUG --> <!-- com.maijinjie.springboot 为根包也就是只要是发生在这个根包下面的所有日志操作行为的权限都是DEBUG -->
<!-- 级别依次为【从高到低】FATAL > ERROR > WARN > INFO > DEBUG > TRACE --> <!-- 级别依次为【从高到低】FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
<logger name="io.github.xxyopen" level="ERROR" additivity="false"> <logger name="io.github.xxyopen" level="ERROR" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/> <appender-ref ref="FILE"/>
</logger> </logger>
</springProfile> </springProfile>

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