手把手教你使用 Spring Boot 3 開發上線一個前後端分離的生產級系統(一) – 介紹

項目簡介

novel 是一套基於時下最新 Java 技術棧 Spring Boot 3 + Vue 3 開發的前後端分離的學習型小說項目,配備詳細的項目教程手把手教你從零開始開發上線一個生產級別的 Java 系統,由小說門戶系統、作家後台管理系統、平台後台管理系統等多個子系統構成。包括小說推薦、作品檢索、小說排行榜、小說閱讀、小說評論、會員中心、作家專區、充值訂閱、新聞發布等功能。

項目地址

開發環境

  • MySQL 8.0
  • Redis 7.0
  • Elasticsearch 8.2.0(可選)
  • RabbitMQ 3.10.2(可選)
  • JDK 17
  • Maven 3.8
  • IntelliJ IDEA 2021.3(可選)
  • Node 16.14

後端技術選型

技術 版本 說明
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));

}