53 Commits

Author SHA1 Message Date
a23f4b202e feat: 处理404异常 2025-04-02 08:19:20 +08:00
cd3a7206a9 perf: instanceof 智能转型 2025-03-19 09:58:04 +08:00
ab166a392a fix: 兼容非HikariDataSource数据源 2025-03-19 08:00:50 +08:00
9d8709ed2d perf: 提前创建数据库连接池
Spring Boot 新版本默认会在第一次请求数据库时创建连接池
2025-03-19 07:53:03 +08:00
60488258f5 perf: 提高接口第一次访问速度
Spring Boot 新版本默认会在第一次访问数据库时才创建连接池
2025-03-19 00:14:00 +08:00
b7bb98db16 build: 修改 Spring AI 版本 2025-03-18 22:34:04 +08:00
e54b656799 修改版本号 2025-02-20 16:43:01 +08:00
dccce83d1c v3.5.0 发布 2025-02-20 16:40:05 +08:00
b2c0340048 文档/日志/优化 2025-02-20 12:19:37 +08:00
9f71aa4a59 feat: 集成 Spring AI 框架,实现基础的 AI 写作功能 2025-02-19 23:44:29 +08:00
295a9096b5 build: 升级 ShardingSphere-JDBC 到 5.5.1
支持 Spring Boot 3.3.0
2025-01-18 00:31:24 +08:00
c46864bbb6 feat: 增加 HTTP 请求和响应的日志记录 2024-09-13 22:48:00 +08:00
d63be23aca perf: /env 端点在 dev 环境下显示属性值 2024-07-17 18:02:01 +08:00
8da6f8263c fix: 初始化 Flyway 历史表
org.flywaydb.core.api.FlywayException: Found non-empty schema(s)
`novel_test` but no schema history table. Use baseline() or set
baselineOnMigrate to true to initialize the schema history table.
2024-07-15 18:07:29 +08:00
b4ce4dd35d perf: 优化SQL脚本文件管理 2024-06-28 07:03:14 +08:00
63760c8e90 feat: 集成 Flyway 2024-06-28 01:46:16 +08:00
e7005b9008 build: 更新java版本到21 2024-06-02 14:02:48 +08:00
876d9b8cbe build: 使用默认的 Spring Boot 内嵌 Web 容器,实现虚拟线程处理请求
Spring Boot v3.3.0-M3 删除了对 Undertow 的虚拟线程支持,因为它会泄漏内存
2024-06-02 12:15:05 +08:00
9da5064a9e perf: 启用虚拟线程
需要在 Java 21 上运行
2024-06-02 08:47:57 +08:00
03b3ca1d83 build: 修改版本号 2024-06-02 08:24:28 +08:00
b0d2adebf6 Merge branch '3.4.x' 2024-06-02 08:18:52 +08:00
f547a8b7d8 build: 3.4.1 发布 2024-06-02 08:04:25 +08:00
e09aad2415 build: 升级spring-boot3至3.3.0 2024-06-01 21:12:55 +08:00
7b4b97569b fix: 分库分表功能失效
shardingsphere-jdbc-core-spring-boot-starter 依赖版本过低
2023-12-21 12:04:48 +08:00
2270072d7e build: 3.4.0 发布 2023-04-25 19:48:24 +08:00
a31edb0c69 feat: 小说章节查询&更新接口 2023-04-25 19:19:44 +08:00
d6df259e94 feat: 小说章节删除接口 2023-04-25 17:43:33 +08:00
1f2d8dc49a feat: 查询会员评论列表接口 2023-04-25 13:10:08 +08:00
90f5780796 feat: 设置 Elasticsearch 的 ssl 认证模式 2023-04-21 16:56:53 +08:00
fc7983236b Update README.md 2023-04-16 11:07:45 +08:00
d5282a3974 Update README.md 2023-04-16 11:06:50 +08:00
74f5b58252 Update README.md 2023-04-16 10:57:14 +08:00
df1719e14c Update README.md 2023-04-16 08:44:16 +08:00
7de1dc6370 Update README.md 2023-04-16 08:34:27 +08:00
65274eae80 Update README.md 2023-03-31 09:37:43 +08:00
46d62d6aa6 fix: fix sun.security.validator.ValidatorException for Elasticsearc
PKIX path building
failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to
find valid certification path to requested target
2023-03-28 08:50:22 +08:00
afeadde581 docs: 调整注释的位置 2023-03-25 14:59:40 +08:00
62d7169304 chore: 关闭 mail 的健康检查 2023-03-25 14:57:32 +08:00
c866702e48 feat: 模版方法模式实现消息发送器 2023-03-25 12:54:15 +08:00
46d03883ed fix(pom.xml): -source X 中不支持 XX
问题描述:使用 maven 编译项目时,部分环境会编译失败,出现 -source X 中不支持 XX
[ERROR]   (请使用
-source Y
或更高版本以启用 XX)

解决方案:需要手动指定 Java 编译器的 -source 和 -target 参数
2023-03-23 01:59:07 +08:00
3c93f90fad fix(interceptor): handler 执行异常时没有清理掉当前线程保存的用户登录信息 2023-03-11 22:38:08 +08:00
xxy
b5be909415 Update README.md 2023-01-11 20:58:06 +08:00
a318b05bee build: Use redisson-spring-boot-starter dependency instead of redisson dependency 2023-01-11 10:15:10 +08:00
fefa5d94ca Update README.md 2023-01-06 13:46:40 +08:00
0ae939da16 build: 升级依赖版本 2023-01-06 13:29:42 +08:00
4b3bcff05f build(pom.xml): 清理 SNAPSHOT 仓库和依赖 2022-09-29 10:51:54 +08:00
a3a2384c95 build: 升级 Spring Boot 版本 2022-09-21 16:57:14 +08:00
31bd2c0bf8 fix: 修复进入作家专区提示访问未授权问题
原因:Spring Boot 3.0.0 在第 4 个 M(里程碑)版本中增加了
ElasticsearchClientConfigurations 配置类,该类改变了 Spring Boot Jackson
的默认配置,导致所有 null 属性都没有返回。

解决方案:将 Spring Boot 的版本修改为 M3 ,因为
SNAPSHOT(快照)版本一直在更新,后面可能会出现类似的问题,暂时不使用 Spring Boot 的 SNAPSHOT 版本,等 GA
版本发布后,再统一升级。
2022-09-21 15:05:29 +08:00
87fdd2e6fc perf: 仅在 dev 环境生成接口文档 2022-09-01 09:50:36 +08:00
3dc7ed59a1 Update README.md 2022-08-20 15:06:50 +08:00
570ef7e7cb perf: 优化配置 2022-08-19 20:33:00 +08:00
3e89d2a363 perf: 优化配置 2022-08-19 19:36:08 +08:00
7c0ff5e9ce Update README.md 2022-08-16 21:37:02 +08:00
48 changed files with 1466 additions and 461 deletions

194
README.md
View File

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

View File

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

162
pom.xml
View File

@ -6,51 +6,38 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.github.xxyopen</groupId>
<artifactId>novel</artifactId>
<version>3.3.0</version>
<version>3.5.1-SNAPSHOT</version>
<name>novel</name>
<description>Spring Boot 3 + Vue 3 构建的前后端分离小说系统</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<spring.version>6.0.0-SNAPSHOT</spring.version>
<java.version>21</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.1</mybatis-plus-generator.version>
<jjwt.version>0.11.5</jjwt.version>
<elasticsearch.version>8.2.0</elasticsearch.version>
<xxl-job.version>2.3.1</xxl-job.version>
<sentinel.version>1.8.4</sentinel.version>
<shardingsphere-jdbc.version>5.1.1</shardingsphere-jdbc.version>
<redisson.version>3.17.4</redisson.version>
<shardingsphere-jdbc.version>5.5.1</shardingsphere-jdbc.version>
<redisson.version>3.19.1</redisson.version>
<spring-boot-admin.version>3.0.0-M1</spring-boot-admin.version>
<springdoc-openapi.version>2.0.0-M4-SNAPSHOT</springdoc-openapi.version>
<springdoc-openapi.version>2.5.0</springdoc-openapi.version>
<logbook.version>3.9.0</logbook.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- Exclude the Tomcat dependency -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use Undertow instead -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
@ -58,7 +45,7 @@
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
<version>${mybatis-plus-generator.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@ -107,15 +94,9 @@
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- elasticsearch 相关 -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- MQ 相关 -->
@ -146,9 +127,14 @@
<!-- ShardingSphere-JDBC -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>${shardingsphere-jdbc.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot 管理和监控 -->
<dependency>
@ -168,7 +154,7 @@
<!-- Redisson 相关 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
@ -185,9 +171,21 @@
<version>${springdoc-openapi.version}</version>
</dependency>
<!-- 邮件 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
@ -205,8 +203,33 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>${logbook.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
@ -221,11 +244,20 @@
</excludes>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!--指定 Java 编译器的 -source 参数 -->
<source>${java.version}</source>
<!--指定 Java 编译器的 -target 参数 -->
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>ali</id>
<id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
@ -234,48 +266,10 @@
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</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>
<pluginRepositories>
<pluginRepository>
<id>ali</id>
<id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
@ -284,22 +278,6 @@
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>

View File

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

View File

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

View File

@ -9,8 +9,10 @@ import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.dto.req.AuthorRegisterReqDto;
import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterUpdateReqDto;
import io.github.xxyopen.novel.dto.resp.BookChapterRespDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.dto.resp.ChapterContentRespDto;
import io.github.xxyopen.novel.service.AuthorService;
import io.github.xxyopen.novel.service.BookService;
import io.swagger.v3.oas.annotations.Operation;
@ -20,12 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.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;
import org.springframework.web.bind.annotation.*;
/**
* 作家后台-作家模块 API 控制器
@ -93,6 +90,37 @@ public class AuthorController {
return bookService.saveBookChapter(dto);
}
/**
* 小说章节删除接口
*/
@Operation(summary = "小说章节删除接口")
@DeleteMapping("book/chapter/{chapterId}")
public RestResp<Void> deleteBookChapter(
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) {
return bookService.deleteBookChapter(chapterId);
}
/**
* 小说章节查询接口
*/
@Operation(summary = "小说章节查询接口")
@GetMapping("book/chapter/{chapterId}")
public RestResp<ChapterContentRespDto> getBookChapter(
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) {
return bookService.getBookChapter(chapterId);
}
/**
* 小说章节更新接口
*/
@Operation(summary = "小说章节更新接口")
@PutMapping("book/chapter/{chapterId}")
public RestResp<Void> updateBookChapter(
@Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId,
@Valid @RequestBody ChapterUpdateReqDto dto) {
return bookService.updateBookChapter(chapterId, dto);
}
/**
* 小说章节发布列表查询接口
*/

View File

@ -8,6 +8,7 @@ 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.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -24,6 +25,7 @@ import java.util.List;
@RestController
@RequestMapping(ApiRouterConsts.API_FRONT_HOME_URL_PREFIX)
@RequiredArgsConstructor
@Slf4j
public class HomeController {
private final HomeService homeService;
@ -34,6 +36,8 @@ public class HomeController {
@Operation(summary = "首页小说推荐查询接口")
@GetMapping("books")
public RestResp<List<HomeBookRespDto>> listHomeBooks() {
// 测试虚拟线程处理请求
log.debug("处理请求的线程:{}", Thread.currentThread());
return homeService.listHomeBooks();
}

View File

@ -1,6 +1,8 @@
package io.github.xxyopen.novel.controller.front;
import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.common.req.PageReqDto;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
@ -8,6 +10,7 @@ import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.req.UserInfoUptReqDto;
import io.github.xxyopen.novel.dto.req.UserLoginReqDto;
import io.github.xxyopen.novel.dto.req.UserRegisterReqDto;
import io.github.xxyopen.novel.dto.resp.UserCommentRespDto;
import io.github.xxyopen.novel.dto.resp.UserInfoRespDto;
import io.github.xxyopen.novel.dto.resp.UserLoginRespDto;
import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto;
@ -19,14 +22,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.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;
import org.springframework.web.bind.annotation.*;
/**
* 前台门户-会员模块 API 控制器
@ -138,4 +134,13 @@ public class UserController {
return userService.getBookshelfStatus(UserHolder.getUserId(), bookId);
}
/**
* 分页查询评论
*/
@Operation(summary = "查询会员评论列表接口")
@GetMapping("comments")
public RestResp<PageRespDto<UserCommentRespDto>> listComments(PageReqDto pageReqDto) {
return bookService.listComments(UserHolder.getUserId(), pageReqDto);
}
}

View File

@ -3,9 +3,7 @@ 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.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
@ -20,6 +18,10 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁 切面
*
@ -28,7 +30,10 @@ import org.springframework.util.StringUtils;
*/
@Aspect
@Component
public record LockAspect(RedissonClient redissonClient) {
@RequiredArgsConstructor
public class LockAspect {
private final RedissonClient redissonClient;
private static final String KEY_PREFIX = "Lock";

View File

@ -3,9 +3,12 @@ package io.github.xxyopen.novel.core.common.exception;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
/**
* 通用的异常处理器
@ -17,6 +20,15 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class CommonExceptionHandler {
/**
* 处理404异常
*/
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handlerNotFound() {
return "404";
}
/**
* 处理数据校验异常
*/

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
package io.github.xxyopen.novel.core.config;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* OpenApi 配置类
*
* @author xiongxiaoyang
* @date 2022/9/1
*/
@Configuration
@Profile("dev")
@OpenAPIDefinition(info = @Info(title = "novel 项目接口文档", version = "v3.2.0", license = @License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0")))
@SecurityScheme(type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME, description = "登录 token")
public class OpenApiConfig {
}

View File

@ -1,26 +0,0 @@
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

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

View File

@ -2,8 +2,6 @@ package io.github.xxyopen.novel.core.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@ -16,7 +14,7 @@ import org.springframework.context.annotation.Configuration;
* @date 2022/5/31
*/
@Configuration
@ConditionalOnProperty(prefix = "xxl.job", name = "enable", havingValue = "true")
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true")
@Slf4j
public class XxlJobConfig {

View File

@ -62,6 +62,11 @@ public class ApiRouterConsts {
*/
public static final String SEARCH_URL_PREFIX = "/search";
/**
* AI模块请求路径前缀
*/
public static final String AI_URL_PREFIX = "/ai";
/**
* 前台门户首页API请求路径前缀
*/
@ -94,4 +99,10 @@ public class ApiRouterConsts {
public static final String API_FRONT_SEARCH_URL_PREFIX =
API_FRONT_URL_PREFIX + SEARCH_URL_PREFIX;
/**
* 作家后台AI相关API请求路径前缀
*/
public static final String API_AUTHOR_AI_URL_PREFIX = API_AUTHOR_URL_PREFIX + AI_URL_PREFIX;
}

View File

@ -0,0 +1,25 @@
package io.github.xxyopen.novel.core.constant;
/**
* 消息发送器的类型
*
* @author xiongxiaoyang
* @date 2023/3/24
*/
public class MessageSenderTypeConsts {
private MessageSenderTypeConsts() {
throw new IllegalStateException("Constant class");
}
/**
* 注册成功的邮件发送器
*/
public static final String REGISTER_MAIL_SENDER = "registerMailSender";
/**
* 秒杀活动的系统通知发送器
*/
public static final String SECKILL_SYS_NOTICE_SENDER = "seckillSysNoticeSender";
}

View File

@ -9,14 +9,15 @@ import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* 认证授权 拦截器:为了注入其它的 Spring beans需要通过 @Component 注解将该拦截器注册到 Spring 上下文
*
@ -31,6 +32,9 @@ public class AuthInterceptor implements HandlerInterceptor {
private final ObjectMapper objectMapper;
/**
* handle 执行前调用
*/
@SuppressWarnings("NullableProblems")
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
@ -60,12 +64,26 @@ public class AuthInterceptor implements HandlerInterceptor {
}
}
/**
* handler 执行后调用,出现异常不调用
*/
@SuppressWarnings("NullableProblems")
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// 清理当前线程保存的用户数据
UserHolder.clear();
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* DispatcherServlet 完全处理完请求后调用,出现异常照常调用
*/
@SuppressWarnings("NullableProblems")
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 清理当前线程保存的用户数据
UserHolder.clear();
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}

View File

@ -9,7 +9,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* Token 解析拦截器
@ -23,6 +22,7 @@ public class TokenParseInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;
@SuppressWarnings("NullableProblems")
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
@ -35,11 +35,16 @@ public class TokenParseInterceptor implements HandlerInterceptor {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
/**
* DispatcherServlet 完全处理完请求后调用,出现异常照常调用
*/
@SuppressWarnings("NullableProblems")
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 清理当前线程保存的用户数据
UserHolder.clear();
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}

View File

@ -21,8 +21,8 @@ import org.springframework.stereotype.Component;
* @date 2022/5/25
*/
@Component
@ConditionalOnProperty(prefix = "spring", name = {"elasticsearch.enable",
"amqp.enable"}, havingValue = "true")
@ConditionalOnProperty(prefix = "spring", name = {"elasticsearch.enabled",
"amqp.enabled"}, havingValue = "true")
@RequiredArgsConstructor
@Slf4j
public class RabbitQueueListener {

View File

@ -13,20 +13,21 @@ import io.github.xxyopen.novel.core.constant.EsConsts;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.es.EsBookDto;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 小说数据同步到 elasticsearch 任务
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true")
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enabled", havingValue = "true")
@Component
@RequiredArgsConstructor
@Slf4j

View File

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

View File

@ -24,7 +24,7 @@ public class BookChapterRespDto implements Serializable {
/**
* 章节ID
* */
*/
@Schema(description = "章节ID")
private Long id;
@ -56,7 +56,7 @@ public class BookChapterRespDto implements Serializable {
* 章节更新时间
*/
@Schema(description = "章节更新时间")
@JsonFormat(pattern = "yyyy/MM/dd HH:dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime chapterUpdateTime;
/**

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import io.github.xxyopen.novel.dao.entity.BookChapter;
import io.github.xxyopen.novel.dao.mapper.BookChapterMapper;
import io.github.xxyopen.novel.dto.resp.BookChapterRespDto;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@ -34,8 +35,14 @@ public class BookChapterCacheManager {
.chapterName(bookChapter.getChapterName())
.chapterWordCount(bookChapter.getWordCount())
.chapterUpdateTime(bookChapter.getUpdateTime())
.isVip(bookChapter.getIsVip())
.build();
}
@CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_CHAPTER_CACHE_NAME)
public void evictBookChapterCache(Long chapterId) {
// 调用此方法自动清除小说章节信息的缓存
}
}

View File

@ -6,6 +6,7 @@ import io.github.xxyopen.novel.core.constant.DatabaseConsts;
import io.github.xxyopen.novel.dao.entity.BookContent;
import io.github.xxyopen.novel.dao.mapper.BookContentMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@ -34,5 +35,11 @@ public class BookContentCacheManager {
return bookContent.getContent();
}
@CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
value = CacheConsts.BOOK_CONTENT_CACHE_NAME)
public void evictBookContentCache(Long chapterId) {
// 调用此方法自动清除小说内容信息的缓存
}
}

View File

@ -8,13 +8,14 @@ import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookChapterMapper;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 小说信息 缓存管理类
*
@ -74,7 +75,7 @@ public class BookInfoCacheManager {
@CacheEvict(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER,
value = CacheConsts.BOOK_INFO_CACHE_NAME)
public void evictBookInfoCache(Long ignoredId) {
public void evictBookInfoCache(Long bookId) {
// 调用此方法自动清除小说信息的缓存
}

View File

@ -0,0 +1,50 @@
package io.github.xxyopen.novel.manager.message;
import io.github.xxyopen.novel.core.config.MailProperties;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
/**
* 抽象的邮件消息发送者
*
* @author xiongxiaoyang
* @date 2023/3/24
*/
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractMailSender extends AbstractMessageSender {
private final MailProperties mailProperties;
private final JavaMailSender mailSender;
@Override
protected void sendMessage(Long toUserId, String messageTitle, String messageContent) {
// TODO 根据消息接收方的用户ID查询出消息接收方的邮件地址
String toEmail = "xxyopen@foxmail.com";
// 开始发送邮件
log.info("发送 HTML 邮件开始:{},{},{}", toEmail, messageTitle, messageContent);
// 使用 MimeMessageMIME 协议
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper;
// MimeMessageHelper 帮助我们设置更丰富的内容
try {
helper = new MimeMessageHelper(message, true);
helper.setFrom(new InternetAddress(mailProperties.username(), mailProperties.nickname(), "UTF-8"));
helper.setTo(toEmail);
helper.setSubject(messageTitle);
// 第二个参数 true 代表支持 html
helper.setText(messageContent, true);
mailSender.send(message);
log.info("发送 HTML 邮件 to {} 成功", toEmail);
} catch (Exception e) {
// 邮件发送失败不会重试
log.error("发送 HTML 邮件 to {} 失败", toEmail, e);
}
}
}

View File

@ -0,0 +1,92 @@
package io.github.xxyopen.novel.manager.message;
/**
* 抽象的消息发送器
* <p>
* 遵循松耦合的设计原则,所有的属性都使用构造函数注入,与 Spring 框架解藕
* <p>
* 所有的消息发送器既可以注册到 Spring 容器中,作为 Spring 的一个组件使用,也可以直接通过 new 对象的方式使用
* <p>
* 每种类型的消息发送时机可能都不一样,不同类型和发送时机的消息格式可能也不一样,所以由各个子类去拓展消息的格式
*
* @author xiongxiaoyang
* @date 2023/3/24
*/
public abstract class AbstractMessageSender implements MessageSender {
private static final String PLACEHOLDER = "{}";
/**
* 定义消息发送的模版,子类不能修改此模版
*/
@Override
public final void sendMessage(Long toUserId, Object... args) {
// 1.获取消息标题模版
String titleTemplate = getTitleTemplate();
// 2.获取消息内容模版
String contentTemplate = getContentTemplate();
// 3.解析消息模版,得到最终需要发送的消息标题
String title = resolveTitle(titleTemplate, args);
// 4.解析消息内容,得到最终需要发送的消息内容
String content = resolveContent(contentTemplate, args);
// 5.发送消息
sendMessage(toUserId, title, content);
}
/**
* 发送消息,具体发送到哪里由子类决定
*
* @param toUserId 消息接收方的用户ID
* @param messageTitle 消息标题
* @param messageContent 消息内容
*/
protected abstract void sendMessage(Long toUserId, String messageTitle, String messageContent);
/**
* 获取消息标题的模版,具体如何制定模版由子类决定
*
* @return 消息标题
*/
protected abstract String getTitleTemplate();
/**
* 获取消息内容的模版,具体如何制定模版由子类决定
*
* @return 消息内容
*/
protected abstract String getContentTemplate();
/**
* 通过给定的参数列表解析消息标题模版,默认固定标题,不需要解析,可以由子类来拓展它的功能
*
* @param titleTemplate 消息标题模版
* @param arguments 用来解析的参数列表
* @return 解析后的消息标题
*/
protected String resolveTitle(String titleTemplate, Object... arguments) {
return titleTemplate;
}
/**
* 通过给定的参数列表解析消息内容模版,默认实现是使用参数列表来替换消息内容模版中的占位符,可以由子类来拓展它的功能
* <p>
* 子类可以根据第一个/前几个参数去数据库中查询动态内容,然后重组参数列表
*
* @param contentTemplate 消息内容模版
* @param args 用来解析的参数列表
* @return 解析后的消息内容
*/
protected String resolveContent(String contentTemplate, Object... args) {
if (args.length > 0) {
StringBuilder formattedContent = new StringBuilder(contentTemplate);
for (Object arg : args) {
int start = formattedContent.indexOf(PLACEHOLDER);
formattedContent.replace(start, start + PLACEHOLDER.length(),
String.valueOf(arg));
}
return formattedContent.toString();
}
return contentTemplate;
}
}

View File

@ -0,0 +1,26 @@
package io.github.xxyopen.novel.manager.message;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 抽象的系统通知发送者
*
* @author xiongxiaoyang
* @date 2023/3/24
*/
@Slf4j
public abstract class AbstractSysNoticeSender extends AbstractMessageSender {
@Override
protected void sendMessage(Long toUserId, String messageTitle, String messageContent) {
// 生成消息的发送时间
LocalDateTime messageDateTime = LocalDateTime.now();
// TODO 在数据库系统通知表中插入一条记录
log.info("系统通知发送成功,{},{},{},{}", toUserId, messageDateTime.format(DateTimeFormatter.ISO_DATE_TIME),
messageTitle, messageContent);
}
}

View File

@ -0,0 +1,21 @@
package io.github.xxyopen.novel.manager.message;
/**
* 消息发送器接口,用来发送各种消息
* <p>
* 消息按类型分系统通知、邮件、短信、小程序通知等,按发送时机分注册成功消息、充值成功消息、活动通知消息、账户封禁消息、小说下架消息等
*
* @author xiongxiaoyang
* @date 2023/3/25
*/
public interface MessageSender {
/**
* 发送消息,支持动态消息标题和动态消息内容
*
* @param toUserId 消息接收方的用户ID
* @param args 用来动态生成消息标题和消息内容的参数列表
*/
void sendMessage(Long toUserId, Object... args);
}

View File

@ -0,0 +1,60 @@
package io.github.xxyopen.novel.manager.message;
import io.github.xxyopen.novel.core.config.MailProperties;
import io.github.xxyopen.novel.core.constant.MessageSenderTypeConsts;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* 注册成功的邮件发送器
*
* @author xiongxiaoyang
* @date 2023/3/24
*/
@Component(value = MessageSenderTypeConsts.REGISTER_MAIL_SENDER)
@EnableConfigurationProperties(MailProperties.class)
public class RegisterMailSender extends AbstractMailSender {
public RegisterMailSender(MailProperties mailProperties, JavaMailSender mailSender) {
super(mailProperties, mailSender);
}
@Override
protected String getTitleTemplate() {
return "欢迎来到小说精品屋";
}
@Override
protected String getContentTemplate() {
return """
<div>
感谢你注册小说精品屋!你的账户现在处于活动状态。
</div>
<ul>
<li> 你的账户电子邮件:{}
<li> 你的账户用户名:{}
</ul>
<div style="padding: 10px 0 50px 0; text-align: center;">
<a style="background: #0274be; color: #fff; padding: 12px 30px; text-decoration: none; border-radius: 3px; letter-spacing: 0.3px;" href="{}" target="_blank" rel="noopener">
登录我们的网站
</a>
</div>
如果你有任何问题,请通过 {} 与我们联系。
""";
}
@Override
protected String resolveContent(String content, Object... args) {
// TODO 去数据库/配置文件中查询网站配置
String websiteLink = "https://www.xxyopen.com";
String websiteEmail = "xxyopen@foxmail.com";
return super.resolveContent(content,
Stream.of(args, new Object[]{websiteLink, websiteEmail}).flatMap(Arrays::stream).toArray());
}
}

View File

@ -0,0 +1,25 @@
package io.github.xxyopen.novel.manager.message;
import io.github.xxyopen.novel.core.constant.MessageSenderTypeConsts;
import org.springframework.stereotype.Component;
/**
* 秒杀活动的系统通知发送器
*
* @author xiongxiaoyang
* @date 2023/3/24
*/
@Component(value = MessageSenderTypeConsts.SECKILL_SYS_NOTICE_SENDER)
public class SeckillSystemNoticeSender extends AbstractSysNoticeSender {
@Override
protected String getTitleTemplate() {
return "秒杀即将开始";
}
@Override
protected String getContentTemplate() {
return "{}秒杀,{}即将开始,不要错过哦!点击 {} 前往。";
}
}

View File

@ -1,8 +1,6 @@
package io.github.xxyopen.novel.manager.mq;
import io.github.xxyopen.novel.core.common.constant.CommonConsts;
import io.github.xxyopen.novel.core.constant.AmqpConsts;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Value;
@ -22,14 +20,14 @@ public class AmqpMsgManager {
private final AmqpTemplate amqpTemplate;
@Value("${spring.amqp.enable}")
private String enableAmqp;
@Value("${spring.amqp.enabled:false}")
private boolean amqpEnabled;
/**
* 发送小说信息改变消息
*/
public void sendBookChangeMsg(Long bookId) {
if (Objects.equals(enableAmqp, CommonConsts.TRUE)) {
if (amqpEnabled) {
sendAmqpMessage(amqpTemplate, AmqpConsts.BookChangeMq.EXCHANGE_NAME, null, bookId);
}
}

View File

@ -5,6 +5,7 @@ import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterUpdateReqDto;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*;
@ -179,4 +180,38 @@ public interface BookService {
* @return 章节分页列表数据
*/
RestResp<PageRespDto<BookChapterRespDto>> listBookChapters(Long bookId, PageReqDto dto);
/**
* 分页查询评论
*
* @param userId 会员ID
* @param pageReqDto 分页参数
* @return 评论分页列表数据
*/
RestResp<PageRespDto<UserCommentRespDto>> listComments(Long userId, PageReqDto pageReqDto);
/**
* 小说章节删除
*
* @param chapterId 章节ID
* @return void
*/
RestResp<Void> deleteBookChapter(Long chapterId);
/**
* 小说章节查询
*
* @param chapterId 章节ID
* @return 章节内容
*/
RestResp<ChapterContentRespDto> getBookChapter(Long chapterId);
/**
* 小说章节更新
*
* @param chapterId 章节ID
* @param dto 更新内容
* @return void
*/
RestResp<Void> updateBookChapter(Long chapterId, ChapterUpdateReqDto dto);
}

View File

@ -11,11 +11,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.RestResp;
import io.github.xxyopen.novel.core.constant.DatabaseConsts;
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.entity.*;
import io.github.xxyopen.novel.dao.mapper.BookChapterMapper;
import io.github.xxyopen.novel.dao.mapper.BookCommentMapper;
import io.github.xxyopen.novel.dao.mapper.BookContentMapper;
@ -23,39 +19,25 @@ import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.AuthorInfoDto;
import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterUpdateReqDto;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.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.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.dto.resp.*;
import io.github.xxyopen.novel.manager.cache.*;
import io.github.xxyopen.novel.manager.dao.UserDaoManager;
import io.github.xxyopen.novel.manager.mq.AmqpMsgManager;
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.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 小说模块 服务实现类
@ -427,6 +409,141 @@ public class BookServiceImpl implements BookService {
.build()).toList()));
}
@Override
public RestResp<PageRespDto<UserCommentRespDto>> listComments(Long userId, PageReqDto pageReqDto) {
IPage<BookComment> page = new Page<>();
page.setCurrent(pageReqDto.getPageNum());
page.setSize(pageReqDto.getPageSize());
QueryWrapper<BookComment> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, userId)
.orderByDesc(DatabaseConsts.CommonColumnEnum.UPDATE_TIME.getName());
IPage<BookComment> bookCommentPage = bookCommentMapper.selectPage(page, queryWrapper);
List<BookComment> comments = bookCommentPage.getRecords();
if (!CollectionUtils.isEmpty(comments)) {
List<Long> bookIds = comments.stream().map(BookComment::getBookId).toList();
QueryWrapper<BookInfo> bookInfoQueryWrapper = new QueryWrapper<>();
bookInfoQueryWrapper.in(DatabaseConsts.CommonColumnEnum.ID.getName(), bookIds);
Map<Long, BookInfo> bookInfoMap = bookInfoMapper.selectList(bookInfoQueryWrapper).stream()
.collect(Collectors.toMap(BookInfo::getId, Function.identity()));
return RestResp.ok(PageRespDto.of(pageReqDto.getPageNum(), pageReqDto.getPageSize(), page.getTotal(),
comments.stream().map(v -> UserCommentRespDto.builder()
.commentContent(v.getCommentContent())
.commentBook(bookInfoMap.get(v.getBookId()).getBookName())
.commentBookPic(bookInfoMap.get(v.getBookId()).getPicUrl())
.commentTime(v.getCreateTime())
.build()).toList()));
}
return RestResp.ok(PageRespDto.of(pageReqDto.getPageNum(), pageReqDto.getPageSize(), page.getTotal(),
Collections.emptyList()));
}
@Transactional(rollbackFor = Exception.class)
@Override
public RestResp<Void> deleteBookChapter(Long chapterId) {
// 1.查询章节信息
BookChapterRespDto chapter = bookChapterCacheManager.getChapter(chapterId);
// 2.查询小说信息
BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(chapter.getBookId());
// 3.删除章节信息
bookChapterMapper.deleteById(chapterId);
// 4.删除章节内容
QueryWrapper<BookContent> bookContentQueryWrapper = new QueryWrapper<>();
bookContentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId);
bookContentMapper.delete(bookContentQueryWrapper);
// 5.更新小说信息
BookInfo newBookInfo = new BookInfo();
newBookInfo.setId(chapter.getBookId());
newBookInfo.setUpdateTime(LocalDateTime.now());
newBookInfo.setWordCount(bookInfo.getWordCount() - chapter.getChapterWordCount());
if (Objects.equals(bookInfo.getLastChapterId(), chapterId)) {
// 设置最新章节信息
QueryWrapper<BookChapter> bookChapterQueryWrapper = new QueryWrapper<>();
bookChapterQueryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, chapter.getBookId())
.orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
BookChapter bookChapter = bookChapterMapper.selectOne(bookChapterQueryWrapper);
Long lastChapterId = 0L;
String lastChapterName = "";
LocalDateTime lastChapterUpdateTime = null;
if (Objects.nonNull(bookChapter)) {
lastChapterId = bookChapter.getId();
lastChapterName = bookChapter.getChapterName();
lastChapterUpdateTime = bookChapter.getUpdateTime();
}
newBookInfo.setLastChapterId(lastChapterId);
newBookInfo.setLastChapterName(lastChapterName);
newBookInfo.setLastChapterUpdateTime(lastChapterUpdateTime);
}
bookInfoMapper.updateById(newBookInfo);
// 6.清理章节信息缓存
bookChapterCacheManager.evictBookChapterCache(chapterId);
// 7.清理章节内容缓存
bookContentCacheManager.evictBookContentCache(chapterId);
// 8.清理小说信息缓存
bookInfoCacheManager.evictBookInfoCache(chapter.getBookId());
// 9.发送小说信息更新的 MQ 消息
amqpMsgManager.sendBookChangeMsg(chapter.getBookId());
return RestResp.ok();
}
@Override
public RestResp<ChapterContentRespDto> getBookChapter(Long chapterId) {
BookChapterRespDto chapter = bookChapterCacheManager.getChapter(chapterId);
String bookContent = bookContentCacheManager.getBookContent(chapterId);
return RestResp.ok(
ChapterContentRespDto.builder()
.chapterName(chapter.getChapterName())
.chapterContent(bookContent)
.isVip(chapter.getIsVip())
.build());
}
@Transactional
@Override
public RestResp<Void> updateBookChapter(Long chapterId, ChapterUpdateReqDto dto) {
// 1.查询章节信息
BookChapterRespDto chapter = bookChapterCacheManager.getChapter(chapterId);
// 2.查询小说信息
BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(chapter.getBookId());
// 3.更新章节信息
BookChapter newChapter = new BookChapter();
newChapter.setId(chapterId);
newChapter.setChapterName(dto.getChapterName());
newChapter.setWordCount(dto.getChapterContent().length());
newChapter.setIsVip(dto.getIsVip());
newChapter.setUpdateTime(LocalDateTime.now());
bookChapterMapper.updateById(newChapter);
// 4.更新章节内容
BookContent newContent = new BookContent();
newContent.setContent(dto.getChapterContent());
newContent.setUpdateTime(LocalDateTime.now());
QueryWrapper<BookContent> bookContentQueryWrapper = new QueryWrapper<>();
bookContentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId);
bookContentMapper.update(newContent, bookContentQueryWrapper);
// 5.更新小说信息
BookInfo newBookInfo = new BookInfo();
newBookInfo.setId(chapter.getBookId());
newBookInfo.setUpdateTime(LocalDateTime.now());
newBookInfo.setWordCount(
bookInfo.getWordCount() - chapter.getChapterWordCount() + dto.getChapterContent().length());
if (Objects.equals(bookInfo.getLastChapterId(), chapterId)) {
// 更新最新章节信息
newBookInfo.setLastChapterName(dto.getChapterName());
newBookInfo.setLastChapterUpdateTime(LocalDateTime.now());
}
bookInfoMapper.updateById(newBookInfo);
// 6.清理章节信息缓存
bookChapterCacheManager.evictBookChapterCache(chapterId);
// 7.清理章节内容缓存
bookContentCacheManager.evictBookContentCache(chapterId);
// 8.清理小说信息缓存
bookInfoCacheManager.evictBookInfoCache(chapter.getBookId());
// 9.发送小说信息更新的 MQ 消息
amqpMsgManager.sendBookChangeMsg(chapter.getBookId());
return RestResp.ok();
}
@Override
public RestResp<BookContentAboutRespDto> getBookContentAbout(Long chapterId) {
log.debug("userId:{}", UserHolder.getUserId());

View File

@ -8,19 +8,20 @@ import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.SearchService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 数据库搜索 服务实现类
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "false")
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enabled", havingValue = "false")
@Service
@RequiredArgsConstructor
@Slf4j

View File

@ -19,22 +19,23 @@ import io.github.xxyopen.novel.dto.es.EsBookDto;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import io.github.xxyopen.novel.service.SearchService;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Elasticsearch 搜索 服务实现类
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true")
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enabled", havingValue = "true")
@Service
@RequiredArgsConstructor
@Slf4j

View File

@ -1,15 +0,0 @@
package org.springframework.core;
import java.io.IOException;
/**
* 兼容 mybatis-plus 3.5.1
* mybatis-plus 的 MybatisSqlSessionFactoryBean 中使用到了这个异常
* Spring 6 开始移除了该异常
*
* @author xiongxiaoyang
* @date 2022/5/12
*/
public class NestedIOException extends IOException {
}

View File

@ -0,0 +1,76 @@
{
"properties": [
{
"name": "spring.elasticsearch.enabled",
"description": "Whether enable elasticsearch or not.",
"type": "java.lang.Boolean"
},
{
"defaultValue": false,
"name": "spring.amqp.enabled",
"description": "Whether enable amqp or not.",
"type": "java.lang.Boolean"
},
{
"name": "xxl.job.enabled",
"description": "Whether enable xxl-job or not.",
"type": "java.lang.Boolean"
},
{
"name": "novel.jwt.secret",
"type": "java.lang.String",
"description": "JWT 密钥."
},
{
"name": "novel.xss.enabled",
"type": "java.lang.Boolean",
"description": "是否开启 XSS 过滤."
},
{
"name": "novel.xss.excludes",
"type": "java.util.List<java.lang.String>",
"description": "XSS 过滤排除链接."
},
{
"name": "novel.file.upload.path",
"type": "java.lang.String",
"description": "上传文件目录."
},
{
"name": "novel.cors.allow-origins",
"type": "java.util.List<java.lang.String>",
"description": "允许跨域的域名."
},
{
"name": "xxl.job.admin.addresses",
"type": "java.lang.String",
"description": "调度中心部署根地址."
},
{
"name": "xxl.job.executor.appname",
"type": "java.lang.String",
"description": "执行器 AppName."
},
{
"name": "xxl.job.executor.logpath",
"type": "java.lang.String",
"description": "执行器运行日志文件存储磁盘路径."
},
{
"name": "xxl.job.accessToken",
"type": "java.lang.String",
"description": "xxl-job accessToken."
},
{
"name": "spring.elasticsearch.ssl.verification-mode",
"type": "java.lang.String",
"description": "设置 ssl 的认证模式,如果该配置项为 none ,说明不需要认证,信任所有的 ssl 证书."
},
{
"defaultValue": true,
"name": "spring.shardingsphere.enabled",
"description": "Whether enable shardingsphere or not.",
"type": "java.lang.Boolean"
}
]
}

View File

@ -1,24 +1,59 @@
#--------------------------通用配置-------------------------
spring:
application:
# 应用名
name: novel
profiles:
# 激活特定配置
active: dev
# 将所有数字转为 String 类型返回,避免前端数据精度丢失的问题
jackson:
generator:
# JSON 序列化时,将所有 Number 类型的属性都转为 String 类型返回,避免前端数据精度丢失的问题。
# 由于 Javascript 标准规定所有数字处理都应使用 64 位 IEEE 754 浮点值完成,
# 结果是某些 64 位整数值无法准确表示(尾数只有 51 位宽)
write-numbers-as-strings: true
servlet:
# 上传文件最大大小
multipart:
# 上传文件最大大小
max-file-size: 5MB
application:
name: novel
# 启用虚拟线程
threads:
virtual:
enabled: true
# 即使所有的用户线程包括虚拟线程都是守护线程的情况下JVM 也不会立即退出,
# Spring Boot 官方建议在开启虚拟线程时设置该属性
main:
keep-alive: true
flyway:
# 是否开启 Flyway
enabled: false
# initialize the schema history table
baseline-on-migrate: true
# url: jdbc:mysql://localhost:3306/novel?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
# user: root
# password: test123456
server:
# 端口号
port: 8888
---
--- #--------------------- Spring AI 配置----------------------
spring:
ai:
openai:
api-key: sk-rrrupturhdofbiqzjutduuiceecpvfqlnvmgcyiaipbdikoi
base-url: https://api.siliconflow.cn
chat:
options:
model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B
--- #---------------------数据库配置---------------------------
spring:
datasource:
url: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
url: jdbc:mysql://localhost:3306/novel?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: test123456
# ShardingSphere-JDBC 配置
@ -27,79 +62,32 @@ spring:
shardingsphere:
# 是否开启分库分表
enabled: false
props:
# 是否在日志中打印 SQL
sql-show: true
# 模式配置
mode:
# 单机模式
type: Standalone
repository:
# 文件持久化
type: File
props:
# 元数据存储路径
path: .shardingsphere
# 使用本地配置覆盖持久化配置
overwrite: true
# 数据源配置
datasource:
names: ds_0
ds_0:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: test123456
# 规则配置
rules:
# 数据分片
sharding:
tables:
# book_content 表
book_content:
# 数据节点
actual-data-nodes: ds_$->{0}.book_content$->{0..9}
# 分表策略
table-strategy:
standard:
# 分片列名称
sharding-column: chapter_id
# 分片算法名称
sharding-algorithm-name: bookContentSharding
sharding-algorithms:
bookContentSharding:
# 行表达式分片算法,使用 Groovy 的表达式,提供对 SQL 语句中的 = 和 IN 的分片操作支持
type: INLINE
props:
# 分片算法的行表达式
algorithm-expression: book_content$->{chapter_id % 10}
config:
activate:
on-profile: dev
---
--- #---------------------中间件配置---------------------------
spring:
# Redis 配置
redis:
host: 127.0.0.1
port: 6379
password: 123456
config:
activate:
on-profile: dev
data:
# Redis 配置
redis:
host: 127.0.0.1
port: 6379
password: test123456
# Elasticsearch 配置
elasticsearch:
# 是否开启 elasticsearch 搜索引擎功能true-开启 false-不开启
enable: false
# 是否开启 Elasticsearch 搜索引擎功能true-开启 false-不开启
enabled: false
uris:
- https://my-deployment-ce7ca3.es.us-central1.gcp.cloud.es.io:9243
username: elastic
password: qTjgYVKSuExX6tWAsDuvuvwl
# 设置 ssl 的认证模式,如果该配置项为 none ,说明不需要认证,信任所有的 ssl 证书。
# ssl:
# verification-mode: none
# Spring AMQP 配置
amqp:
# 是否开启 Spring AMQPtrue-开启 false-不开启
enable: false
enabled: false
# RabbitMQ 配置
rabbitmq:
addresses: "amqp://guest:guest@47.106.243.172"
@ -117,7 +105,7 @@ spring:
xxl:
job:
# 是否开启 XXL-JOBtrue-开启 false-不开启
enable: false
enabled: false
admin:
### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
addresses: http://127.0.0.1:8080/xxl-job-admin
@ -129,13 +117,9 @@ xxl:
### xxl-job, access token
accessToken: 123
---
--- #----------------------安全配置----------------------------
spring:
config:
activate:
on-profile: dev
# Spring Boot 应用管理和监控
# Spring Boot 应用管理和监控
boot:
admin:
client:
@ -167,7 +151,6 @@ management:
exposure:
# 公开所有的 Web 端点
include: "*"
# 端点启用配置
endpoint:
logfile:
@ -175,7 +158,6 @@ management:
enabled: true
# 外部日志文件路径
external-file: logs/novel.log
info:
env:
# 公开所有以 info. 开头的环境属性
@ -187,16 +169,55 @@ management:
elasticsearch:
# 关闭 elasticsearch 的健康检查
enabled: false
mail:
# 关闭 mail 的健康检查
enabled: false
--- #--------------------接口文档配置---------------------------
springdoc:
api-docs:
enabled: false
---
--- #----------------------邮箱配置-----------------------------
#邮箱服务器
spring:
config:
activate:
on-profile: dev
mail:
host: smtp.163.com
#发件人昵称
nickname: xxyopen
#邮箱账户
username: xxx@163.com
#邮箱第三方授权码
password: xxx
#编码类型
default-encoding: UTF-8
port: 465
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: rue
socketFactory:
port: 465
class: javax.net.ssl.SSLSocketFactory
fallback: false
# 项目配置
--- #----------------------Logbook配置-----------------------------
logbook:
format:
# 输出格式
style: http
obfuscate:
headers:
# 隐藏 Authorization 头信息
- Authorization
parameters:
# 隐藏密码参数
- password
--- #---------------------自定义配置----------------------------
novel:
# 跨域配置
cors:
@ -204,7 +225,7 @@ novel:
allow-origins:
- http://localhost:1024
- http://localhost:8080
# JWT密钥
# JWT 密钥
jwt:
secret: E66559580A1ADF48CDD928516062F12E
# XSS 过滤配置
@ -221,4 +242,37 @@ novel:
path: /Users/xiongxiaoyang/upload
--- #------------------- dev 特定配置---------------------------
spring:
config:
activate:
on-profile: dev
# 开启 SpringDoc 接口文档
springdoc:
api-docs:
enabled: true
# /env 端点显示属性值
management:
endpoint:
env:
show-values: when_authorized
--- #------------------- test 特定配置--------------------------
spring:
config:
activate:
on-profile: test
--- #-------------------- prod 特定配置-------------------------
spring:
config:
activate:
on-profile: prod
data:
# Redis 配置
redis:
host: 127.0.0.1
port: 6379
password:

View File

@ -62,6 +62,10 @@
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
<logger name="org.zalando.logbook" level="TRACE" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
</springProfile>
<springProfile name="prod">

View File

@ -1,3 +0,0 @@
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: 123456

View File

@ -0,0 +1,55 @@
mode:
# 单机模式
type: Standalone
# 元数据持久化
repository:
# 数据库持久化
type: JDBC
props:
# 元数据存储类型
provider: H2
jdbc_url: jdbc:h2:./.h2/shardingsphere
# 数据源配置
dataSources:
ds_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/novel?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: test123456
# 配置其他数据源
# 规则配置
rules:
# 配置单表规则
- !SINGLE
tables:
- "*.*"
# 配置分片规则
- !SHARDING
tables: # 数据分片规则配置
book_content:
# 分库策略,缺省表示使用默认分库策略
actualDataNodes: ds_${1}.book_content${0..9}
# 分表策略
tableStrategy:
standard:
# 分片列名称
shardingColumn: chapter_id
# 分片算法名称
shardingAlgorithmName: bookContentSharding
shardingAlgorithms:
bookContentSharding:
# 行表达式分片算法,使用 Groovy 的表达式,提供对 SQL 语句中的 = 和 IN 的分片操作支持
type: INLINE
props:
# 分片算法的行表达式
algorithm-expression: book_content${chapter_id % 10}
props:
# 是否在日志中打印 SQL
sql-show: true