novel 是一套基於時下最新 Java 技術棧 Spring Boot 3 + Vue 3 開發的前後端分離的學習型小說專案,配備詳細的專案教學手把手教你從零開始開發上線一個生產級別的 Java 系統,由小說入口網站、作家後臺管理系統、平臺後臺管理系統等多個子系統構成。包括小說推薦、作品檢索、小說排行榜、小說閱讀、小說評論、會員中心、作家專區、充值訂閱、新聞釋出等功能。
技術 | 版本 | 說明 |
---|---|---|
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 | 資料庫服務 |
Elasticsearch | 8.2.0 | 搜尋引擎服務 |
RabbitMQ | 3.10.2 | 開源訊息中介軟體 |
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));
}