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

2022-05-28 12:02:18

專案簡介

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));

}