24 Commits

Author SHA1 Message Date
20a2d64cc1 chore: 修改版本号 2022-05-26 16:05:09 +08:00
b8950b5be2 docs: update README.md 2022-05-25 10:35:06 +08:00
3a1990fbbd docs: update README.md 2022-05-25 10:21:28 +08:00
e5592b85dd feat: 集成 Spring AMQP,使用 RabbitMQ 发送小说更新消息
默认关闭,通过 spring.rabbitmq.enable 配置属性来开启
2022-05-25 09:39:25 +08:00
a8e2e2d5c9 feat: 设置 Elasticsearch 搜索关键词高亮显示 2022-05-24 19:53:51 +08:00
23fa646cd6 fix: 修复 ES 搜索的相关 BUG 2022-05-24 17:00:33 +08:00
471a24a330 docs: update README.md 2022-05-24 10:19:41 +08:00
774d3e3556 docs: update README.md 2022-05-24 10:18:06 +08:00
a056248398 docs: update README.md 2022-05-24 09:23:39 +08:00
ab2bead9b3 feat: 实现 Elasticsearch 8.2.0 高级搜索功能 2022-05-24 00:44:57 +08:00
4c331224a4 docs: update README.md 2022-05-23 22:00:15 +08:00
d45dc3d015 feat: 集成 Elasticsearch 8.2.0 2022-05-23 21:57:07 +08:00
61d261d277 feat: 增加小说章节发布接口 2022-05-23 13:22:42 +08:00
45fdb58ab3 feat: 增加小说发布接口 2022-05-23 12:40:32 +08:00
879673bca5 feat: 增加作家注册功能 2022-05-23 12:06:17 +08:00
e3cf41fbd8 fix: 图片格式校验 2022-05-22 10:30:55 +08:00
1b9627eddc feat: 评论列表增加用户头像返回 2022-05-22 10:07:50 +08:00
5e01c1c5a1 fix: 设置上传文件最大大小 2022-05-22 10:02:59 +08:00
61cc3b5f07 feat: 增加文件访问拦截器 2022-05-22 09:09:11 +08:00
6b366348c0 feat: 增加用户信息查询接口 2022-05-22 08:52:46 +08:00
282755f7ab feat: 增加图片上传接口 2022-05-22 08:23:40 +08:00
c431683540 perf: 规范变量名 2022-05-21 19:38:17 +08:00
28a2717231 fix: 解决 Json 格式请求参数的 XSS 攻击 2022-05-21 08:26:20 +08:00
f0fc2d8e1e docs: update README.md 2022-05-21 06:05:26 +08:00
66 changed files with 1801 additions and 289 deletions

View File

@ -12,15 +12,15 @@ 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-cloud) [码云](https://gitee.com/novel_dev_team/novel-cloud)
- 前端小说门户系统项目(更新中):[GitHub](https://github.com/201206030/novel-front-web) [码云](https://gitee.com/novel_dev_team/novel-front-web)
- 前端作家管理系统项目:待上线
- 前端平台后台管理系统项目:待上线
- 前端项目(更新中):[GitHub](https://github.com/201206030/novel-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/)
## 开发环境
- MySQL 8.0
- Redis 7.0
- Elasticsearch 8.2.0(可选)
- RabbitMQ 3.10.2(可选)
- JDK 17
- Maven 3.8
- IntelliJ IDEA 2021.3(可选)
@ -28,22 +28,24 @@ 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) |
| Mybatis | 3.5.9 | ORM 框架 | http://www.mybatis.org | [进入](https://mybatis.org/mybatis-3/zh/index.html) |
| MyBatis-Plus | 3.5.1 | Mybatis 增强工具 | https://baomidou.com/ | [进入](https://baomidou.com/pages/24112f/) |
| JJWT | 0.11.5 | JWT 登录支持 | https://github.com/jwtk/jjwt | - |
| Lombok | 1.18.24 | 简化对象封装工具 | https://github.com/projectlombok/lombok | [进入](https://projectlombok.org/features/all) |
| Caffeine | 3.1.0 | 本地缓存支持 | https://github.com/ben-manes/caffeine | [进入](https://github.com/ben-manes/caffeine/wiki/Home-zh-CN) |
| Redis | 7.0 | 分布式缓存支持 | https://redis.io | [进入](https://redis.io/docs) |
| MySQL | 8.0 | 数据库服务 | https://www.mysql.com | [进入](https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html) |
| Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 | https://undertow.io | [进入](https://undertow.io/documentation.html) |
| Docker | - | 应用容器引擎 | https://www.docker.com/ | - |
| Jenkins | - | 自动化部署工具 | https://github.com/jenkinsci/jenkins | - |
| Sonarqube | - | 代码质量控制 | https://www.sonarqube.org/ | - |
| 技术 | 版本 | 说明 | 官网 | 学习 |
|:----------------|:--------------:|---------------------| --------------------------------------- | :-------------------------------------------------: |
| Spring Boot | 3.0.0-SNAPSHOT | 容器 + MVC 框架 | https://spring.io/projects/spring-boot | [进入](https://youdoc.github.io/course/novel/11.html) |
| Mybatis | 3.5.9 | ORM 框架 | http://www.mybatis.org | [进入](https://mybatis.org/mybatis-3/zh/index.html) |
| MyBatis-Plus | 3.5.1 | Mybatis 增强工具 | https://baomidou.com/ | [进入](https://baomidou.com/pages/24112f/) |
| JJWT | 0.11.5 | JWT 登录支持 | https://github.com/jwtk/jjwt | - |
| Lombok | 1.18.24 | 简化对象封装工具 | https://github.com/projectlombok/lombok | [进入](https://projectlombok.org/features/all) |
| Caffeine | 3.1.0 | 本地缓存支持 | https://github.com/ben-manes/caffeine | [进入](https://github.com/ben-manes/caffeine/wiki/Home-zh-CN) |
| Redis | 7.0 | 分布式缓存支持 | https://redis.io | [进入](https://redis.io/docs) |
| MySQL | 8.0 | 数据库服务 | https://www.mysql.com | [进入](https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html) |
| Elasticsearch | 8.2.0 | 搜索引擎服务 | https://www.elastic.co | [进入](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) |
| RabbitMQ | 3.10.2 | 开源消息中间件 | https://www.rabbitmq.com | [进入](https://www.rabbitmq.com/tutorials/tutorial-one-java.html) |
| Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 | https://undertow.io | [进入](https://undertow.io/documentation.html) |
| Docker | - | 应用容器引擎 | https://www.docker.com/ | - |
| Jenkins | - | 自动化部署工具 | https://github.com/jenkinsci/jenkins | - |
| Sonarqube | - | 代码质量控制 | https://www.sonarqube.org/ | - |
<!--| SpringFox Swagger2 | ?(不支持 Spring 6 | Spring项目接口文档生成工具 | https://github.com/springfox/springfox | - | -->
**注:更多热门新技术待集成。**
## 前端技术选型
| 技术 | 版本 | 说明 | 官网 | 学习 |
@ -85,6 +87,7 @@ io
| +- constant -- 业务相关常量
| +- filter -- 过滤器
| +- interceptor -- 拦截器
| +- json -- JSON 相关的包,包括序列化器和反序列化器
| +- task -- 定时任务
| +- util -- 业务相关工具
| +- wrapper -- 装饰器
@ -174,7 +177,7 @@ git clone https://gitee.com/novel_dev_team/novel.git
1. 新建数据库(建议 novel
2. 解压后端源码`sql/novel.sql.zip`压缩包,得到数据库结构文件`novel_struc.sql`和数据库小说数据文件`novel_data.sql`
2. 解压后端源码`doc/sql/novel.sql.zip`压缩包,得到数据库结构文件`novel_struc.sql`和数据库小说数据文件`novel_data.sql`
3. 导入`novel_struct.sql`数据库结构文件

62
doc/es/book.http Normal file
View File

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

346
pom.xml
View File

@ -1,176 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.github.xxyopen</groupId>
<artifactId>novel</artifactId>
<version>3.0.0</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>
<jjwt.version>0.11.5</jjwt.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>
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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.github.xxyopen</groupId>
<artifactId>novel</artifactId>
<version>3.1.0</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>
<jjwt.version>0.11.5</jjwt.version>
<elasticsearch.version>8.2.0</elasticsearch.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>
<!-- 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>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- mybatis-plus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
<scope>test</scope>
</dependency>
<!-- mybatis-plus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
<scope>test</scope>
</dependency>
<!-- 缓存相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- 缓存相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- JWT 相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- JWT 相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- 请求参数校验相关 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- 请求参数校验相关 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 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>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<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>
</repositories>
<pluginRepositories>
<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>
<!-- MQ 相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<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>
</repositories>
<pluginRepositories>
<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

@ -9,12 +9,14 @@ import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.Map;
@SpringBootApplication
@MapperScan("io.github.xxyopen.novel.dao.mapper")
@EnableCaching
@EnableScheduling
@Slf4j
public class NovelApplication {

View File

@ -0,0 +1,57 @@
package io.github.xxyopen.novel.controller.author;
import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
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.service.AuthorService;
import io.github.xxyopen.novel.service.BookService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 作家后台-作家模块 API 控制器
* @author xiongxiaoyang
* @date 2022/5/23
*/
@RestController
@RequestMapping(ApiRouterConsts.API_AUTHOR_URL_PREFIX)
@RequiredArgsConstructor
public class AuthorController {
private final AuthorService authorService;
private final BookService bookService;
/**
* 作家注册接口
*/
@PostMapping("register")
public RestResp<Void> register(@Valid @RequestBody AuthorRegisterReqDto dto) {
dto.setUserId(UserHolder.getUserId());
return authorService.register(dto);
}
/**
* 小说发布接口
*/
@PostMapping("book")
public RestResp<Void> publishBook(@Valid @RequestBody BookAddReqDto dto) {
return bookService.saveBook(dto);
}
/**
* 小说章节发布接口
*/
@PostMapping("book/chapter")
public RestResp<Void> publishBookChapter(@Valid @RequestBody ChapterAddReqDto dto) {
return bookService.saveBookChapter(dto);
}
}

View File

@ -6,6 +6,7 @@ import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.*;
import io.github.xxyopen.novel.service.BookService;
import io.github.xxyopen.novel.service.SearchService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -13,7 +14,7 @@ import java.security.NoSuchAlgorithmException;
import java.util.List;
/**
* 小说模块 API 接口
* 前台门户-小说模块 API 控制器
*
* @author xiongxiaoyang
* @date 2022/5/14
@ -25,6 +26,8 @@ public class BookController {
private final BookService bookService;
private final SearchService searchService;
/**
* 小说分类列表查询接口
*/
@ -38,7 +41,7 @@ public class BookController {
*/
@GetMapping("search_list")
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
return bookService.searchBooks(condition);
return searchService.searchBooks(condition);
}
/**

View File

@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 首页模块 API 接口
* 前台门户-首页模块 API 控制器
*
* @author xiongxiaoyang
* @date 2022/5/12

View File

@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 新闻模块 API 接口
* 前台门户-新闻模块 API 控制器
*
* @author xiongxiaoyang
* @date 2022/5/12

View File

@ -5,14 +5,13 @@ import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.dto.resp.ImgVerifyCodeRespDto;
import io.github.xxyopen.novel.service.ResourceService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* 资源图片/视频/文档)相关 控制器
* 前台门户-资源(图片/视频/文档)模块 API 控制器
*
* @author xiongxiaoyang
* @date 2022/5/17
@ -32,4 +31,12 @@ public class ResourceController {
return resourceService.getImgVerifyCode();
}
/**
* 图片上传接口
* */
@PostMapping("/image")
RestResp<String> uploadImage(@RequestParam("file") MultipartFile file) {
return resourceService.uploadImage(file);
}
}

View File

@ -7,6 +7,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.UserInfoRespDto;
import io.github.xxyopen.novel.dto.resp.UserLoginRespDto;
import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto;
import io.github.xxyopen.novel.service.BookService;
@ -16,7 +17,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 会员模块相关 控制器
* 前台门户-会员模块 API 控制器
*
* @author xiongxiaoyang
* @date 2022/5/17
@ -46,6 +47,13 @@ public class UserController {
return userService.login(dto);
}
/**
* 用户信息查询接口
*/
@GetMapping
public RestResp<UserInfoRespDto> getUserInfo() {
return userService.getUserInfo(UserHolder.getUserId());
}
/**
* 用户信息修改接口

View File

@ -15,7 +15,7 @@ import org.springframework.stereotype.Component;
public class AdminAuthStrategy implements AuthStrategy {
@Override
public void auth(String token) throws BusinessException {
public void auth(String token, String requestUri) throws BusinessException {
// TODO 平台后台 token 校验
}
}

View File

@ -5,7 +5,7 @@ import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.dto.UserInfoDto;
import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager;
import org.springframework.util.StringUtils;
import java.util.Objects;
@ -20,12 +20,12 @@ public interface AuthStrategy {
/**
* 请求用户认证
* 如果后面需要扩展到对每一个URI都进行权限控制那么此方法可以加一个参数来接收用户请求的URI
*
* @param token 登录 token
* @param requestUri 请求的 URI
* @throws BusinessException 认证失败则抛出业务异常
*/
void auth(String token) throws BusinessException;
void auth(String token, String requestUri) throws BusinessException;
/**
* 前台多系统单点登录统一账号认证(门户系统、作家系统以及后面会扩展的漫画系统和视频系统等)

View File

@ -2,13 +2,15 @@ package io.github.xxyopen.novel.core.auth;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.dto.AuthorInfoDto;
import io.github.xxyopen.novel.manager.AuthorInfoCacheManager;
import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.AuthorInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Objects;
/**
@ -27,14 +29,22 @@ public class AuthorAuthStrategy implements AuthStrategy {
private final AuthorInfoCacheManager authorInfoCacheManager;
/**
* 不需要进行作家权限认证的 URI
* */
private static final List<String> EXCLUDE_URI = List.of(ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/register");
@Override
public void auth(String token) throws BusinessException {
public void auth(String token, String requestUri) throws BusinessException {
// 统一账号认证
Long userId = authSSO(jwtUtils, userInfoCacheManager, token);
if(EXCLUDE_URI.contains(requestUri)){
// 该请求不需要进行作家权限认证
return;
}
// 作家权限认证
AuthorInfoDto authorInfo = authorInfoCacheManager.getAuthor(userId);
if(Objects.isNull(authorInfo)){
if (Objects.isNull(authorInfo)) {
// 作家账号不存在,无权访问作家专区
throw new BusinessException(ErrorCodeEnum.USER_UN_AUTH);
}

View File

@ -2,7 +2,7 @@ package io.github.xxyopen.novel.core.auth;
import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@ -21,7 +21,7 @@ public class FrontAuthStrategy implements AuthStrategy {
private final UserInfoCacheManager userInfoCacheManager;
@Override
public void auth(String token) throws BusinessException {
public void auth(String token, String requestUri) throws BusinessException {
// 统一账号认证
authSSO(jwtUtils,userInfoCacheManager,token);
}

View File

@ -13,11 +13,14 @@ public class CommonConsts {
* 是
* */
public static final Integer YES = 1;
public static final String TRUE = "true";
/**
* 否
* */
public static final Integer NO = 0;
public static final String FALSE = "false";
/**
* 性别常量

View File

@ -92,6 +92,16 @@ public enum ErrorCodeEnum {
* */
USER_COMMENTED("A2001","用户已发表评论"),
/**
* 用户上传文件异常
* */
USER_UPLOAD_FILE_ERROR("A0700","用户上传文件异常"),
/**
* 用户上传文件类型不匹配
* */
USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701","用户上传文件类型不匹配"),
/**
* 一级宏观错误码,系统执行出错
* */

View File

@ -0,0 +1,44 @@
package io.github.xxyopen.novel.core.config;
import io.github.xxyopen.novel.core.constant.AmqpConsts;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* AMQP 配置类
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
@Configuration
public class AmqpConfig {
/**
* 小说信息改变交换机
*/
@Bean
public FanoutExchange bookChangeExchange() {
return new FanoutExchange(AmqpConsts.BookChangeMq.EXCHANGE_NAME);
}
/**
* Elasticsearch book 索引更新队列
*/
@Bean
public Queue esBookUpdateQueue() {
return new Queue(AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE);
}
/**
* Elasticsearch book 索引更新队列绑定到小说信息改变交换机
*/
@Bean
public Binding esBookUpdateQueueBinding() {
return BindingBuilder.bind(esBookUpdateQueue()).to(bookChangeExchange());
}
}

View File

@ -0,0 +1,35 @@
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 org.elasticsearch.client.RestClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* elasticsearch 相关配置
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true")
@RequiredArgsConstructor
public class EsConfig {
@Bean
public ElasticsearchClient elasticsearchClient(RestClient restClient) {
// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
// And create the API client
return new ElasticsearchClient(transport);
}
}

View File

@ -1,7 +1,9 @@
package io.github.xxyopen.novel.core.config;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.core.interceptor.AuthInterceptor;
import io.github.xxyopen.novel.core.interceptor.FileInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@ -19,11 +21,18 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor frontAuthInterceptor;
private final AuthInterceptor authInterceptor;
private final FileInterceptor fileInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(frontAuthInterceptor)
// 文件访问拦截
registry.addInterceptor(fileInterceptor)
.addPathPatterns(SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY + "**");
// 权限认证拦截
registry.addInterceptor(authInterceptor)
// 拦截会员中心相关请求接口
.addPathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/**"
// 拦截作家后台相关请求接口
@ -33,7 +42,7 @@ public class WebConfig implements WebMvcConfigurer {
// 放行登录注册相关请求接口
.excludePathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/register"
, ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/login"
, ApiRouterConsts.API_AUTHOR_URL_PREFIX + "/register"
,ApiRouterConsts.API_ADMIN_URL_PREFIX + "/login");
}
}

View File

@ -0,0 +1,33 @@
package io.github.xxyopen.novel.core.constant;
/**
* AMQP 相关常量
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
public class AmqpConsts {
/**
* 小说信息改变 MQ
* */
public static class BookChangeMq{
/**
* 小说信息改变交换机
* */
public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE";
/**
* Elasticsearch book 索引更新的队列
* */
public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE";
/**
* Redis book 缓存更新的队列
* */
public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE";
}
}

View File

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

View File

@ -32,6 +32,11 @@ public class SystemConfigConsts {
* */
public static final String NOVEL_ADMIN_KEY = "admin";
/**
* 图片上传目录
* */
public static final String IMAGE_UPLOAD_DIRECTORY = "/image/";
/**
* 常量类实例化异常信息
* */

View File

@ -43,13 +43,13 @@ public class AuthInterceptor implements HandlerInterceptor {
String requestUri = request.getRequestURI();
// 根据请求的 URI 得到认证策略
String authStrategyName = requestUri.substring(ApiRouterConsts.API_URL_PREFIX.length() + 1);
authStrategyName = authStrategyName.substring(0,authStrategyName.indexOf("/"));
authStrategyName = String.format("%sAuthStrategy",authStrategyName);
String subUri = requestUri.substring(ApiRouterConsts.API_URL_PREFIX.length() + 1);
String systemName = subUri.substring(0,subUri.indexOf("/"));
String authStrategyName = String.format("%sAuthStrategy",systemName);
// 开始认证
try {
authStrategy.get(authStrategyName).auth(token);
authStrategy.get(authStrategyName).auth(token,requestUri);
return HandlerInterceptor.super.preHandle(request, response, handler);
}catch (BusinessException exception){
// 认证失败

View File

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

View File

@ -0,0 +1,33 @@
package io.github.xxyopen.novel.core.json.deserializer;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
/**
* JSON 全局反序列化器
*
* @author xiongxiaoyang
* @date 2022/5/21
*/
@JsonComponent
public class GlobalJsonDeserializer {
/**
* 字符串反序列化器
* 过滤特殊字符,解决 XSS 攻击
*/
public static class StringDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
return jsonParser.getValueAsString()
.replace("<", "&lt;")
.replace(">", "&gt;");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ public class BookSearchReqDto extends PageReqDto {
/**
* 作品方向
*/
private Byte workDirection;
private Integer workDirection;
/**
* 分类ID
@ -34,12 +34,12 @@ public class BookSearchReqDto extends PageReqDto {
/**
* 是否收费1收费0免费
*/
private Byte isVip;
private Integer isVip;
/**
* 小说更新状态0连载中1已完结
*/
private Byte bookStatus;
private Integer bookStatus;
/**
* 字数最小值
@ -64,5 +64,5 @@ public class BookSearchReqDto extends PageReqDto {
/**
* 排序字段
*/
private String sort = "last_chapter_update_time desc";
private String sort;
}

View File

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

View File

@ -19,7 +19,7 @@ public class UserInfoUptReqDto {
@Length(min = 2,max = 10)
private String nickName;
@Pattern(regexp="^/[^\s]{10,}\\.(png|jpg|jpeg|gif|bpm)$")
@Pattern(regexp="^/[^\s]{10,}\\.(png|PNG|jpg|JPG|jpeg|JPEG|gif|GIF|bpm|BPM)$")
private String userPhoto;
@Min(value = 0)

View File

@ -18,7 +18,6 @@ import java.util.List;
@Builder
public class BookCommentRespDto {
private Long commentTotal;
private List<CommentInfo> comments;
@ -36,11 +35,11 @@ public class BookCommentRespDto {
private Long commentUserId;
private String commentUserPhoto;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime commentTime;
}
}

View File

@ -1,7 +1,6 @@
package io.github.xxyopen.novel.dto.resp;
import lombok.Builder;
import lombok.Data;
import lombok.*;
/**
* 小说信息 响应DTO
@ -10,6 +9,8 @@ import lombok.Data;
* @date 2022/5/15
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BookInfoRespDto {

View File

@ -0,0 +1,30 @@
package io.github.xxyopen.novel.dto.resp;
import lombok.Builder;
import lombok.Data;
/**
* 用户信息 响应DTO
*
* @author xiongxiaoyang
* @date 2022/5/22
*/
@Data
@Builder
public class UserInfoRespDto {
/**
* 昵称
* */
private String nickName;
/**
* 用户头像
* */
private String userPhoto;
/**
* 用户性别
* */
private Integer userSex;
}

View File

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

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import io.github.xxyopen.novel.core.constant.CacheConsts;
import io.github.xxyopen.novel.dao.entity.BookChapter;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;
@ -9,6 +9,7 @@ import io.github.xxyopen.novel.dao.mapper.BookChapterMapper;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@ -29,11 +30,20 @@ public class BookInfoCacheManager {
private final BookChapterMapper bookChapterMapper;
/**
* 查询小说信息并放入缓存中
* 从缓存中查询小说信息先判断缓存中是否已存在存在则直接从缓存中取否则执行方法体中的逻辑后缓存结果
*/
@Cacheable(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER
, value = CacheConsts.BOOK_INFO_CACHE_NAME)
public BookInfoRespDto getBookInfo(Long id) {
return cachePutBookInfo(id);
}
/**
* 缓存小说信息不管缓存中是否存在都执行方法体中的逻辑然后缓存起来
* */
@CachePut(cacheManager = CacheConsts.CAFFEINE_CACHE_MANAGER
, value = CacheConsts.BOOK_INFO_CACHE_NAME)
public BookInfoRespDto cachePutBookInfo(Long id) {
// 查询基础信息
BookInfo bookInfo = bookInfoMapper.selectById(id);
// 查询首章ID
@ -62,6 +72,8 @@ public class BookInfoCacheManager {
.build();
}
/**
* 查询每个类别下最新更新的 500 个小说ID列表并放入缓存中 1 个小时
*/

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import io.github.xxyopen.novel.core.constant.CacheConsts;
import io.github.xxyopen.novel.dao.entity.UserInfo;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.dao;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.DatabaseConsts;

View File

@ -0,0 +1,52 @@
package io.github.xxyopen.novel.manager.mq;
import io.github.xxyopen.novel.core.common.constant.CommonConsts;
import io.github.xxyopen.novel.core.constant.AmqpConsts;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Objects;
/**
* AMQP 消息管理类
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
@Component
@RequiredArgsConstructor
public class AmqpMsgManager {
private final AmqpTemplate amqpTemplate;
@Value("${spring.amqp.enable}")
private String enableAmqp;
/**
* 发送小说信息改变消息
*/
public void sendBookChangeMsg(Long bookId) {
if (Objects.equals(enableAmqp, CommonConsts.TRUE)) {
sendAmqpMessage(amqpTemplate, AmqpConsts.BookChangeMq.EXCHANGE_NAME, null, bookId);
}
}
private void sendAmqpMessage(AmqpTemplate amqpTemplate, String exchange, String routingKey, Object message) {
// 如果在事务中则在事务执行完成后再发送,否则可以直接发送
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
amqpTemplate.convertAndSend(exchange, routingKey, message);
}
});
return;
}
amqpTemplate.convertAndSend(exchange, routingKey, message);
}
}

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.redis;
import io.github.xxyopen.novel.core.common.util.ImgVerifyCodeUtils;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -0,0 +1,21 @@
package io.github.xxyopen.novel.service;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.req.AuthorRegisterReqDto;
/**
* 作家模块 业务服务类
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
public interface AuthorService {
/**
* 作家注册
*
* @param dto 注册参数
* @return void
*/
RestResp<Void> register(AuthorRegisterReqDto dto);
}

View File

@ -1,8 +1,8 @@
package io.github.xxyopen.novel.service;
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.BookSearchReqDto;
import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*;
@ -17,14 +17,6 @@ import java.util.List;
*/
public interface BookService {
/**
* 小说搜索
*
* @param condition 搜索条件
* @return 搜索结果
*/
RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition);
/**
* 小说点击榜查询
*
@ -150,4 +142,18 @@ public interface BookService {
* @return void
* */
RestResp<Void> updateComment(Long userId, Long id, String content);
/**
* 小说信息保存
* @param dto 小说信息
* @return void
* */
RestResp<Void> saveBook(BookAddReqDto dto);
/**
* 小说章节信息保存
* @param dto 章节信息
* @return void
* */
RestResp<Void> saveBookChapter(ChapterAddReqDto dto);
}

View File

@ -2,6 +2,7 @@ package io.github.xxyopen.novel.service;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.resp.ImgVerifyCodeRespDto;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@ -16,7 +17,15 @@ public interface ResourceService {
/**
* 获取图片验证码
*
* @throws IOException 验证码图片生成失败
* @return Base64编码的图片
*/
RestResp<ImgVerifyCodeRespDto> getImgVerifyCode() throws IOException;
/**
* 图片上传
* @param file 需要上传的图片
* @return 图片访问路径
* */
RestResp<String> uploadImage(MultipartFile file);
}

View File

@ -0,0 +1,24 @@
package io.github.xxyopen.novel.service;
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.BookSearchReqDto;
import io.github.xxyopen.novel.dto.resp.BookInfoRespDto;
/**
* 搜索 服务类
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
public interface SearchService {
/**
* 小说搜索
*
* @param condition 搜索条件
* @return 搜索结果
*/
RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition);
}

View File

@ -4,6 +4,7 @@ import io.github.xxyopen.novel.core.common.resp.RestResp;
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.UserInfoRespDto;
import io.github.xxyopen.novel.dto.resp.UserLoginRespDto;
import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto;
@ -65,4 +66,11 @@ public interface UserService {
* @return 0-不在书架 1-已在书架
*/
RestResp<Integer> getBookshelfStatus(Long userId, String bookId);
/**
* 用户信息查询
* @param userId 用户ID
* @return 用户信息
*/
RestResp<UserInfoRespDto> getUserInfo(Long userId);
}

View File

@ -0,0 +1,57 @@
package io.github.xxyopen.novel.service.impl;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dao.entity.AuthorInfo;
import io.github.xxyopen.novel.dao.mapper.AuthorInfoMapper;
import io.github.xxyopen.novel.dto.AuthorInfoDto;
import io.github.xxyopen.novel.dto.req.AuthorRegisterReqDto;
import io.github.xxyopen.novel.manager.cache.AuthorInfoCacheManager;
import io.github.xxyopen.novel.service.AuthorService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* 作家模块 服务实现类
*
* @author xiongxiaoyang
* @date 2022/5/23
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthorServiceImpl implements AuthorService {
private final AuthorInfoCacheManager authorInfoCacheManager;
private final AuthorInfoMapper authorInfoMapper;
@Override
public RestResp<Void> register(AuthorRegisterReqDto dto) {
// 校验该用户是否已注册为作家
AuthorInfoDto author = authorInfoCacheManager.getAuthor(dto.getUserId());
if (Objects.nonNull(author)) {
// 该用户已经是作家,直接返回
return RestResp.ok();
}
// 保存作家注册信息
AuthorInfo authorInfo = new AuthorInfo();
authorInfo.setUserId(dto.getUserId());
authorInfo.setChatAccount(dto.getChatAccount());
authorInfo.setEmail(dto.getEmail());
authorInfo.setInviteCode("0");
authorInfo.setTelPhone(dto.getTelPhone());
authorInfo.setPenName(dto.getPenName());
authorInfo.setWorkDirection(dto.getWorkDirection());
authorInfo.setCreateTime(LocalDateTime.now());
authorInfo.setUpdateTime(LocalDateTime.now());
authorInfoMapper.insert(authorInfo);
// 清除作家缓存
authorInfoCacheManager.evictAuthorCache();
return RestResp.ok();
}
}

View File

@ -1,31 +1,34 @@
package io.github.xxyopen.novel.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
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.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;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.req.BookSearchReqDto;
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.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*;
import io.github.xxyopen.novel.manager.*;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
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;
/**
@ -49,34 +52,21 @@ public class BookServiceImpl implements BookService {
private final BookContentCacheManager bookContentCacheManager;
private final AuthorInfoCacheManager authorInfoCacheManager;
private final BookInfoMapper bookInfoMapper;
private final BookChapterMapper bookChapterMapper;
private final BookContentMapper bookContentMapper;
private final BookCommentMapper bookCommentMapper;
private final UserDaoManager userDaoManager;
private static final Integer REC_BOOK_COUNT = 4;
private final AmqpMsgManager amqpMsgManager;
@Override
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
Page<BookInfoRespDto> page = new Page<>();
page.setCurrent(condition.getPageNum());
page.setSize(condition.getPageSize());
List<BookInfo> bookInfos = bookInfoMapper.searchBooks(page, condition);
return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), page.getTotal()
, bookInfos.stream().map(v -> BookInfoRespDto.builder()
.id(v.getId())
.bookName(v.getBookName())
.categoryId(v.getCategoryId())
.categoryName(v.getCategoryName())
.authorId(v.getAuthorId())
.authorName(v.getAuthorName())
.wordCount(v.getWordCount())
.lastChapterName(v.getLastChapterName())
.build()).toList()));
}
private static final Integer REC_BOOK_COUNT = 4;
@Override
public RestResp<List<BookRankRespDto>> listVisitRankBooks() {
@ -244,12 +234,13 @@ public class BookServiceImpl implements BookService {
// 查询评论用户信息,并设置需要返回的评论用户名
List<Long> userIds = bookComments.stream().map(BookComment::getUserId).toList();
List<UserInfo> userInfos = userDaoManager.listUsers(userIds);
Map<Long, String> userInfoMap = userInfos.stream().collect(Collectors.toMap(UserInfo::getId, UserInfo::getUsername));
Map<Long, UserInfo> userInfoMap = userInfos.stream().collect(Collectors.toMap(UserInfo::getId, Function.identity()));
List<BookCommentRespDto.CommentInfo> commentInfos = bookComments.stream()
.map(v -> BookCommentRespDto.CommentInfo.builder()
.id(v.getId())
.commentUserId(v.getUserId())
.commentUser(userInfoMap.get(v.getUserId()))
.commentUser(userInfoMap.get(v.getUserId()).getUsername())
.commentUserPhoto(userInfoMap.get(v.getUserId()).getUserPhoto())
.commentContent(v.getCommentContent())
.commentTime(v.getCreateTime()).build()).toList();
bookCommentRespDto.setComments(commentInfos);
@ -279,6 +270,80 @@ public class BookServiceImpl implements BookService {
return RestResp.ok();
}
@Override
public RestResp<Void> saveBook(BookAddReqDto dto) {
BookInfo bookInfo = new BookInfo();
// 设置作家信息
AuthorInfoDto author = authorInfoCacheManager.getAuthor(UserHolder.getUserId());
bookInfo.setAuthorId(author.getId());
bookInfo.setAuthorName(author.getPenName());
// 设置其他信息
bookInfo.setWorkDirection(dto.getWorkDirection());
bookInfo.setCategoryId(dto.getCategoryId());
bookInfo.setCategoryName(dto.getCategoryName());
bookInfo.setBookName(dto.getBookName());
bookInfo.setPicUrl(dto.getPicUrl());
bookInfo.setBookDesc(dto.getBookDesc());
bookInfo.setIsVip(dto.getIsVip());
bookInfo.setScore(0);
bookInfo.setCreateTime(LocalDateTime.now());
bookInfo.setUpdateTime(LocalDateTime.now());
// 保存小说信息
bookInfoMapper.insert(bookInfo);
return RestResp.ok();
}
@Transactional(rollbackFor = Exception.class)
@Override
public RestResp<Void> saveBookChapter(ChapterAddReqDto dto) {
// 1) 保存章节相关信息到小说章节表
// a) 查询最新章节号
int chapterNum = 0;
QueryWrapper<BookChapter> chapterQueryWrapper = new QueryWrapper<>();
chapterQueryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID,dto.getBookId())
.orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
.last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
BookChapter bookChapter = bookChapterMapper.selectOne(chapterQueryWrapper);
if(Objects.nonNull(bookChapter)){
chapterNum = bookChapter.getChapterNum() + 1;
}
// b) 设置章节相关信息并保存
BookChapter newBookChapter = new BookChapter();
newBookChapter.setBookId(dto.getBookId());
newBookChapter.setChapterName(dto.getChapterName());
newBookChapter.setChapterNum(chapterNum);
newBookChapter.setWordCount(dto.getChapterContent().length());
newBookChapter.setIsVip(dto.getIsVip());
newBookChapter.setCreateTime(LocalDateTime.now());
newBookChapter.setUpdateTime(LocalDateTime.now());
bookChapterMapper.insert(newBookChapter);
// 2) 保存章节内容到小说内容表
BookContent bookContent = new BookContent();
bookContent.setContent(dto.getChapterContent());
bookContent.setChapterId(newBookChapter.getId());
bookContent.setCreateTime(LocalDateTime.now());
bookContent.setUpdateTime(LocalDateTime.now());
bookContentMapper.insert(bookContent);
// 3) 更新小说表最新章节信息和小说总字数信息
// a) 更新小说表关于最新章节的信息
BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(dto.getBookId());
BookInfo newBookInfo = new BookInfo();
newBookInfo.setId(dto.getBookId());
newBookInfo.setLastChapterId(newBookChapter.getId());
newBookInfo.setLastChapterName(newBookChapter.getChapterName());
newBookInfo.setLastChapterUpdateTime(LocalDateTime.now());
newBookInfo.setWordCount(bookInfo.getWordCount() + newBookChapter.getWordCount());
newBookChapter.setUpdateTime(LocalDateTime.now());
bookInfoMapper.updateById(newBookInfo);
// b) 刷新小说信息缓存
bookInfoCacheManager.cachePutBookInfo(dto.getBookId());
// c) 发送小说信息更新的 MQ 消息
amqpMsgManager.sendBookChangeMsg(dto.getBookId());
return RestResp.ok();
}
@Override
public RestResp<BookContentAboutRespDto> getBookContentAbout(Long chapterId) {
// 查询章节信息

View File

@ -0,0 +1,51 @@
package io.github.xxyopen.novel.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.xxyopen.novel.core.common.resp.PageRespDto;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dao.entity.BookInfo;
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 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")
@Service
@RequiredArgsConstructor
@Slf4j
public class DbSearchServiceImpl implements SearchService {
private final BookInfoMapper bookInfoMapper;
@Override
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
Page<BookInfoRespDto> page = new Page<>();
page.setCurrent(condition.getPageNum());
page.setSize(condition.getPageSize());
List<BookInfo> bookInfos = bookInfoMapper.searchBooks(page, condition);
return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), page.getTotal()
, bookInfos.stream().map(v -> BookInfoRespDto.builder()
.id(v.getId())
.bookName(v.getBookName())
.categoryId(v.getCategoryId())
.categoryName(v.getCategoryName())
.authorId(v.getAuthorId())
.authorName(v.getAuthorName())
.wordCount(v.getWordCount())
.lastChapterName(v.getLastChapterName())
.build()).toList()));
}
}

View File

@ -0,0 +1,168 @@
package io.github.xxyopen.novel.service.impl;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.json.JsonData;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
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.EsConsts;
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 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")
@Service
@RequiredArgsConstructor
@Slf4j
public class EsSearchServiceImpl implements SearchService {
private final ElasticsearchClient esClient;
@SneakyThrows
@Override
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
SearchResponse<EsBookDto> response = esClient.search(s -> {
SearchRequest.Builder searchBuilder = s.index(EsConsts.BookIndex.INDEX_NAME);
// 构建检索条件
buildSearchCondition(condition, searchBuilder);
// 排序
if (!StringUtils.isBlank(condition.getSort())) {
searchBuilder.sort(o ->
o.field(f -> f.field(StringUtils
.underlineToCamel(condition.getSort().split(" ")[0]))
.order(SortOrder.Desc))
);
}
// 分页
searchBuilder.from((condition.getPageNum() - 1) * condition.getPageSize())
.size(condition.getPageSize());
// 设置高亮显示
searchBuilder.highlight(h -> h.fields(EsConsts.BookIndex.FIELD_BOOK_NAME
, t -> t.preTags("<em style='color:red'>").postTags("</em>"))
.fields(EsConsts.BookIndex.FIELD_AUTHOR_NAME
, t -> t.preTags("<em style='color:red'>").postTags("</em>")));
return searchBuilder;
},
EsBookDto.class
);
TotalHits total = response.hits().total();
List<BookInfoRespDto> list = new ArrayList<>();
List<Hit<EsBookDto>> hits = response.hits().hits();
for (Hit<EsBookDto> hit : hits) {
EsBookDto book = hit.source();
assert book != null;
if (!CollectionUtils.isEmpty(hit.highlight().get(EsConsts.BookIndex.FIELD_BOOK_NAME))) {
book.setBookName(hit.highlight().get(EsConsts.BookIndex.FIELD_BOOK_NAME).get(0));
}
if (!CollectionUtils.isEmpty(hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME))) {
book.setAuthorName(hit.highlight().get(EsConsts.BookIndex.FIELD_AUTHOR_NAME).get(0));
}
list.add(BookInfoRespDto.builder()
.id(book.getId())
.bookName(book.getBookName())
.categoryId(book.getCategoryId())
.categoryName(book.getCategoryName())
.authorId(book.getAuthorId())
.authorName(book.getAuthorName())
.wordCount(book.getWordCount())
.lastChapterName(book.getLastChapterName())
.build());
}
assert total != null;
return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), total.value(), list));
}
/**
* 构建检索条件
*/
private void buildSearchCondition(BookSearchReqDto condition, SearchRequest.Builder searchBuilder) {
BoolQuery boolQuery = BoolQuery.of(b -> {
if (!StringUtils.isBlank(condition.getKeyword())) {
// 关键词匹配
b.must((q -> q.multiMatch(t -> t
.fields(EsConsts.BookIndex.FIELD_BOOK_NAME + "^2"
, EsConsts.BookIndex.FIELD_AUTHOR_NAME + "^1.8"
, EsConsts.BookIndex.FIELD_BOOK_DESC + "^0.1")
.query(condition.getKeyword())
)
));
}
// 精确查询
if (Objects.nonNull(condition.getWorkDirection())) {
b.must(TermQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_WORK_DIRECTION)
.value(condition.getWorkDirection())
)._toQuery());
}
if (Objects.nonNull(condition.getCategoryId())) {
b.must(TermQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_CATEGORY_ID)
.value(condition.getCategoryId())
)._toQuery());
}
// 范围查询
if (Objects.nonNull(condition.getWordCountMin())) {
b.must(RangeQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_WORD_COUNT)
.gte(JsonData.of(condition.getWordCountMin()))
)._toQuery());
}
if (Objects.nonNull(condition.getWordCountMax())) {
b.must(RangeQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_WORD_COUNT)
.lt(JsonData.of(condition.getWordCountMax()))
)._toQuery());
}
if (Objects.nonNull(condition.getUpdateTimeMin())) {
b.must(RangeQuery.of(m -> m
.field(EsConsts.BookIndex.FIELD_LAST_CHAPTER_UPDATE_TIME)
.gte(JsonData.of(condition.getUpdateTimeMin().getTime()))
)._toQuery());
}
return b;
});
searchBuilder.query(q -> q.bool(boolQuery));
}
}

View File

@ -3,8 +3,8 @@ package io.github.xxyopen.novel.service.impl;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.resp.HomeBookRespDto;
import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto;
import io.github.xxyopen.novel.manager.FriendLinkCacheManager;
import io.github.xxyopen.novel.manager.HomeBookCacheManager;
import io.github.xxyopen.novel.manager.cache.FriendLinkCacheManager;
import io.github.xxyopen.novel.manager.cache.HomeBookCacheManager;
import io.github.xxyopen.novel.service.HomeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

View File

@ -8,7 +8,7 @@ import io.github.xxyopen.novel.dao.entity.NewsInfo;
import io.github.xxyopen.novel.dao.mapper.NewsContentMapper;
import io.github.xxyopen.novel.dao.mapper.NewsInfoMapper;
import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto;
import io.github.xxyopen.novel.manager.NewsCacheManager;
import io.github.xxyopen.novel.manager.cache.NewsCacheManager;
import io.github.xxyopen.novel.service.NewsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

View File

@ -1,14 +1,27 @@
package io.github.xxyopen.novel.service.impl;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.dto.resp.ImgVerifyCodeRespDto;
import io.github.xxyopen.novel.manager.VerifyCodeManager;
import io.github.xxyopen.novel.manager.redis.VerifyCodeManager;
import io.github.xxyopen.novel.service.ResourceService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
/**
* 资源(图片/视频/文档)相关服务实现类
@ -18,10 +31,14 @@ import java.io.IOException;
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ResourceServiceImpl implements ResourceService {
private final VerifyCodeManager verifyCodeManager;
@Value("${novel.file.upload.path}")
private String fileUploadPath;
@Override
public RestResp<ImgVerifyCodeRespDto> getImgVerifyCode() throws IOException {
String sessionId = IdWorker.get32UUID();
@ -31,4 +48,32 @@ public class ResourceServiceImpl implements ResourceService {
.build());
}
@SneakyThrows
@Override
public RestResp<String> uploadImage(MultipartFile file) {
LocalDateTime now = LocalDateTime.now();
String savePath =
SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY
+ now.format(DateTimeFormatter.ofPattern("yyyy")) + File.separator
+ now.format(DateTimeFormatter.ofPattern("MM")) + File.separator
+ now.format(DateTimeFormatter.ofPattern("dd"));
String oriName = file.getOriginalFilename();
assert oriName != null;
String saveFileName = IdWorker.get32UUID() + oriName.substring(oriName.lastIndexOf("."));
File saveFile = new File(fileUploadPath + savePath, saveFileName);
if (!saveFile.getParentFile().exists()) {
boolean isSuccess = saveFile.getParentFile().mkdirs();
if (!isSuccess) {
throw new BusinessException(ErrorCodeEnum.USER_UPLOAD_FILE_ERROR);
}
}
file.transferTo(saveFile);
if (Objects.isNull(ImageIO.read(saveFile))) {
// 上传的文件不是图片
Files.delete(saveFile.toPath());
throw new BusinessException(ErrorCodeEnum.USER_UPLOAD_FILE_TYPE_NOT_MATCH);
}
return RestResp.ok(savePath + File.separator + saveFileName);
}
}

View File

@ -17,9 +17,10 @@ import io.github.xxyopen.novel.dao.mapper.UserInfoMapper;
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.UserInfoRespDto;
import io.github.xxyopen.novel.dto.resp.UserLoginRespDto;
import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto;
import io.github.xxyopen.novel.manager.VerifyCodeManager;
import io.github.xxyopen.novel.manager.redis.VerifyCodeManager;
import io.github.xxyopen.novel.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@ -157,4 +158,14 @@ public class UserServiceImpl implements UserService {
: CommonConsts.NO
);
}
@Override
public RestResp<UserInfoRespDto> getUserInfo(Long userId) {
UserInfo userInfo = userInfoMapper.selectById(userId);
return RestResp.ok(UserInfoRespDto.builder()
.nickName(userInfo.getNickName())
.userSex(userInfo.getUserSex())
.userPhoto(userInfo.getUserPhoto())
.build());
}
}

View File

@ -6,14 +6,14 @@ spring:
jackson:
generator:
write-numbers-as-strings: true
servlet:
# 上传文件最大大小
multipart:
max-file-size: 5MB
server:
port: 8888
---
spring:
datasource:
@ -24,10 +24,9 @@ spring:
activate:
on-profile: dev
---
spring:
# Redis 配置
redis:
host: 127.0.0.1
port: 6379
@ -35,7 +34,29 @@ spring:
config:
activate:
on-profile: dev
# Elasticsearch 配置
elasticsearch:
# 是否开启 elasticsearch 搜索引擎功能true-开启 false-不开启
enable: false
uris:
- https://my-deployment-ce7ca3.es.us-central1.gcp.cloud.es.io:9243
username: elastic
password: qTjgYVKSuExX6tWAsDuvuvwl
amqp:
# 是否开启 Spring AMQPtrue-开启 false-不开启
enable: false
# RabbitMQ 配置
rabbitmq:
addresses: "amqp://guest:guest@47.106.243.172"
virtual-host: novel
template:
retry:
# 开启重试
enabled: true
# 最大重试次数
max-attempts: 3
# 第一次和第二次重试之间的持续时间
initial-interval: "3s"
---
spring:
@ -61,7 +82,11 @@ novel:
# 排除链接
excludes:
- /system/notice/*
file:
# 文件上传配置
upload:
# 上传路径
path: /Users/xiongxiaoyang/upload

View File

@ -7,7 +7,8 @@
id,category_id,category_name,book_name,author_id,author_name,word_count,last_chapter_name
from book_info where word_count > 0
<if test="condition.keyword != null and condition.keyword != ''">
and (book_name like concat('%',#{condition.keyword},'%') or author_name like concat('%',#{condition.keyword},'%'))
and (book_name like concat('%',#{condition.keyword},'%') or author_name like
concat('%',#{condition.keyword},'%'))
</if>
<if test="condition.workDirection != null">
and work_direction = #{condition.workDirection}
@ -30,8 +31,9 @@
<if test="condition.updateTimeMin != null">
and last_chapter_update_time >= #{condition.updateTimeMin}
</if>
order by ${condition.sort}
<if test="condition.sort != null">
order by ${condition.sort}
</if>
</select>
<update id="addVisitCount">