项目简介
novel 是一套基于时下最新 Java 技术栈 Spring Boot 3 + Vue 3 开发的前后端分离学习型开源项目,配备详细的项目开发文档手把手教你从零开始开发上线一个生产级别的 Java 系统,由小说门户系统、作家后台管理系统、平台后台管理系统等多个子系统构成。包括小说推荐、作品检索、小说排行榜、小说阅读、小说评论、会员中心、作家专区、充值订阅、新闻发布等功能。
项目地址
- 后端项目(更新中):GitHub| 码云
- 前端项目(更新中):GitHub |码云
- 线上应用版:GitHub |码云| 演示地址
- 微服务版:GitHub |码云
开发环境
- MySQL 8.0
- Redis 7.0
- Elasticsearch 8.2.0(可选)
- RabbitMQ 3.10.2(可选)
- XXL-JOB 2.3.1(可选)
- JDK 17
- Maven 3.8
- IntelliJ IDEA 2021.3(可选)
- Node 16.14
注:Elasticsearch、RabbitMQ 和 XXL-JOB 默认关闭,可通过 application.yml 配置文件中相应的enable
配置属性开启。
后端技术选型
技术 | 版本 | 说明 |
---|---|---|
Spring Boot | 3.0.0-SNAPSHOT | 容器 + MVC 框架 |
Mybatis | 3.5.9 | ORM 框架 |
MyBatis-Plus | 3.5.1 | Mybatis 增强工具 |
JJWT | 0.11.5 | JWT 登录支持 |
Lombok | 1.18.24 | 简化对象封装工具 |
Caffeine | 3.1.0 | 本地缓存支持 |
Redis | 7.0 | 分布式缓存支持 |
MySQL | 8.0 | 数据库服务 |
ShardingSphere-JDBC | 5.1.1 | 数据库分库分表支持 |
Elasticsearch | 8.2.0 | 搜索引擎服务 |
RabbitMQ | 3.10.2 | 开源消息中间件 |
XXL-JOB | 2.3.1 | 分布式任务调度平台 |
Sentinel | 1.8.4 | 流量控制组件 |
Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 |
Docker | - | 应用容器引擎 |
Jenkins | - | 自动化部署工具 |
Sonarqube | - | 代码质量控制 |
注:更多热门新技术待集成。
前端技术选型
技术 | 版本 | 说明 |
---|---|---|
Vue.js | 3.2.13 | 渐进式 JavaScript 框架 |
Vue Router | 4.0.15 | Vue.js 的官方路由 |
axios | 0.27.2 | 基于 promise 的网络请求库 |
element-plus | 2.2.0 | 基于 Vue 3,面向设计师和开发者的组件库 |
示例代码
代码严格遵守阿里编码规约。
|
/** |
|
* 小说搜索 |
|
*/ |
|
@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(condition.getSort()).order(SortOrder.Desc)) |
|
); |
|
} |
|
// 分页 |
|
searchBuilder.from((condition.getPageNum() - 1) * condition.getPageSize()) |
|
.size(condition.getPageSize()); |
|
|
|
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(); |
|
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()); |
|
} |
|
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)); |
|
|
|
} |