1. 程式人生 > 實用技巧 >第一章 專案介紹和工程搭建

第一章 專案介紹和工程搭建

第一章 專案介紹和工程搭建

學習目標

  • 熟悉移動端應用系統的架構設計
  • 熟悉大型軟體系統設計中的各種圖形結構
  • 熟悉資料庫分庫分表設計技巧
  • 熟悉Spring boot2.0+JavaConfig專案封裝配置方式
  • 完成文章列表的後臺開發

1. 專案介紹

1.1專案背景

隨著智慧手機的普及,人們更加習慣於通過手機來看新聞。由於生活節奏的加快,很多人只能利用碎片時間來獲取資訊,因此,對於移動資訊客戶端的需求也越來越高。黑馬頭條專案正是在這樣背景下開發出來。黑馬頭條專案採用當下火熱的微服務+大資料技術架構實現。本專案主要著手於獲取最新最熱新聞資訊,通過大資料分析使用者喜好精確推送諮詢新聞

1.2 專案概述

黑馬頭條專案是對線上教育平臺業務進行大資料統計分析的系統。碎片化、切換頻繁、社交化和個性化現如今成為人們閱讀行為的標籤。黑馬頭條對海量資訊進行蒐集,通過系統計算分類,分析使用者的興趣進行推送從而滿足使用者的需求。

1.3 專案術語定義

  • 專案:泛指黑馬頭條整個專案或某一專案模組

  • 工程:泛指黑馬頭條某一專案的原始碼工程

  • 使用者:泛指黑馬頭條APP使用者端使用者

  • 自媒體人:泛指通過黑馬自媒體系統傳送文章的使用者

  • 管理員:泛指黑馬頭條管理系統的使用使用者

  • App:泛指黑馬頭條APP

  • WeMedia:泛指黑馬頭條自媒體系統

  • Admin:泛指黑馬頭條管理系統

2. 專案需求

功能需求專案模組結構

2.1 APP主要功能大綱

  • 頻道欄:使用者可以通過此功能新增自己感興趣的頻道,在新增標籤時,系統可依據使用者喜好進行推薦

  • 文章列表:需要顯示文章標題、文章圖片、評論數等資訊,且需要監控文章是否在APP端展現的行為

  • 搜尋文章:聯想使用者想搜尋的內容,並記錄使用者的歷史搜尋資訊

  • 個人中心:使用者可以在其個人中心檢視收藏、關注的人、以及系統設定等功能

  • 檢視文章:使用者點選文章進入檢視文章頁面,在此頁面上可進行點贊、評論、不喜歡、分享等操作;除此之外還需要收集使用者檢視文章的時間,是否看我等行為資訊

  • 實名認證:使用者可以進行身份證認證和實名認證,實名認證之後即可成為自媒體人,在平臺上釋出文章

  • 註冊登入:登入時,驗證內容為手機號登入/註冊,通過手機號驗證碼進行登入/註冊,首次登入使用者自動註冊賬號。

2.2 APP用例圖(主要功能)

2.3 WEMEDIA功能大綱

  • 內容管理:自媒體使用者管理文章頁面,可以根據條件進行篩選,文章包含草稿、已釋出、未通過、已撤回狀態。使用者可以對文章進行修改,上/下架操作、檢視文章狀態等操作

  • 評論管理:管理文章評論頁面,顯示使用者已釋出的全部文章,可以檢視文章總評論數和粉絲評論數,可以對文章進行關閉評論等操作

  • 素材管理:管理自媒體文章釋出的圖片,便於使用者釋出帶有多張圖片的文章

  • 圖文資料:自媒體人釋出文章的資料:閱讀數、評論數、收藏了、轉發量,使用者可以檢視對應文章的閱讀資料

  • 粉絲畫像:內容包括:粉絲性別分佈、粉絲年齡分佈、粉絲終端分佈、粉絲喜歡分類分佈

2.4 WEMEDIA用例圖(主要功能)

2.5 ADMIN功能大綱

  • 使用者管理:系統後臺用來維護使用者資訊,可以對使用者進行增刪改查操作,對於違規使用者可以進行凍結操

  • 使用者稽核:管理員稽核使用者資訊頁面,使用者稽核分為身份稽核和實名稽核,身份稽核是對使用者的身份資訊進行稽核,包括但不限於工作資訊、資質資訊、經歷資訊等;實名認證是對使用者實名身份進行認證

  • 內容管理:管理員查詢現有文章,並對文章進行新增、刪除、修改、置頂等操作

  • 內容稽核:管理員稽核自媒體人釋出的內容,包括但不限於文章文字、圖片、敏感資訊等

  • 頻道管理:管理頻道分類介面,可以新增頻道,檢視頻道,新增或修改頻道關聯的標籤

  • 網站統計:統計內容包括:日活使用者、訪問量、新增使用者、訪問量趨勢、熱門搜尋、使用者地區分佈等資料

  • 內容統計:統計內容包括:文章採集量、釋出量、閱讀量、閱讀時間、評論量、轉發量、圖片量等資料

  • 許可權管理:超級管理員對後臺管理員賬號進行新增或刪除角色操作

2.6 ADMIN用例圖(主要功能)

2.7 其它需求

2.8 互動需求

3. 專案技術介紹

3.1 技術棧-基礎六層技術

基礎六層中包括前端(Weex、Vue、Echarts、WS)、閘道器(GateWay)、DevOps(單元測試、程式碼規範)等重難點技術

  • Weex+Vue+WebSocket :使用Weex跨平臺開發工具,整合整合VUE框架,完成黑馬頭條移動端功能開發,並整合WebSocket實現即時訊息(文章推薦、私信)的推送

  • Vue+Echarts : 自媒體系統使用Vue開發關鍵,整合Echarts圖表框架,完成相關粉絲畫像、資料分析等功能

  • Vue+Echarts+WebSocket : 管理系統也是使用Vue開發,整合Echarts,完成網站統計、內容統計等功能,整合WebSocket,實現系統看板實時資料自動化更新

  • Spring-Cloud-Gateway : 微服務之前架設的閘道器服務,實現服務註冊中的API請求路由,以及控制流速控制和熔斷處理都是常用的架構手段,而這些功能Gateway天然支援

  • PMD&P3C : 靜態程式碼掃描工具,在專案中掃描專案程式碼,檢查異常點、優化點、程式碼規範等,為開發團隊提供規範統一,提升專案程式碼質量

  • Junit : 在持續整合思想中,單元測試偏向自動化過程,專案通過Junit+Maven的整合實現這種過程

3.2 技術棧-服務四層技術

服務四層中包括中介軟體(Kafka、Mycat)、計算(Spark、Neo4j、Hive)、索引、微服務、大資料儲存等重難點技術

  • 運用Spring Boot快速開發框架,構建專案工程;並結合Spring Cloud全家桶技術,實現後端個人中心、自媒體、管理中心等微服務。
  • 運用WebMagic爬蟲技術,完善系統內容自動化採集
  • 運用Kafka完成內部系統訊息通知;與客戶端系統訊息通知;以及實時資料計算
  • 運用MyCat資料庫中介軟體計算,對系統資料進行分開分表,提升系統資料層效能
  • 運用Redis快取技術,實現熱資料的計算,NoSession等功能,提升系統性能指標
  • 運用Zoookeeper技術,完成大資料節點之後的協調與管理,提升系統儲存層高可用
  • 使用Mysql儲存使用者資料,以保證上層資料查詢的高效能
  • 使用Mongo儲存使用者熱資料,以保證使用者熱資料高擴充套件和高效能指標
  • 使用FastDFS作為靜態資源儲存器,在其上實現熱靜態資源快取、淘汰等功能
  • 運用Habse技術,儲存系統中的冷資料,保證系統資料的可靠性
  • 運用ES搜尋技術,對冷資料、文章資料建立索引,以保證冷資料、文章查詢效能
  • 運用Sqoop、Kettle等工具,實現大資料的離線入倉;或者資料備份到Hadoop
  • 運用Spark+Hive進行離線資料分析,實現系統中各類統計報表
  • 運用Spark Streaming + Hive+Kafka實現實時資料分析與應用;比如文章推薦
  • 運用Neo4j知識圖譜技術,分析資料關係,產出知識結果,並應用到上層業務中,以幫助使用者、自媒體、運營效果/能力提升。比如粉絲等級計算
  • 運用AI技術,來完成系統自動化功能,以提升效率及節省成本。比如實名認證自動化

3.1 技術棧-分佈

  • 【分層】 :專案技術按分層分類,涉及前端、後臺、資料採集、中介軟體、業務資料儲存、大資料儲存、大資料應用、知識圖譜、AI等9個層面的技術
  • 【領域】 :專案技術按領域分類,涉及MVVM、圖表、跨終端、微服務、訊息中介軟體、資料庫中介軟體、爬蟲、大資料儲存、大資料流計算、大資料分析、知識圖譜等22個領域的主流技術
  • 【技術】 :專案共涉及22個主要技術框架的綜合運用

4. 資料庫設計

4.1 ER圖設計

er圖設計劃分出了9個庫,各個庫主要解決的是某一個特定的業務。

資料庫設計規範,詳見資料資料夾下《黑馬頭條-資料庫規範設計說明書.md》檔案。

PowerDesinger工具使用,詳見資料資料夾下'powerdesinger的基本使用'資料夾裡的《powerdesinger的基本使用》檔案。

4.2 分庫設計

​ 黑馬頭條專案採用的分庫分表設計,因為業務比較複雜,後期的訪問量巨大,為了分攤資料庫的壓力,整個專案用的不只是一個數據庫。其中核心庫有5個,每一個數據庫解決的是一個業務點,非常接近與實際專案設計。

  • AppInfo app資訊庫,主要儲存使用者資訊,文章資訊,使用者動態,使用者評論,使用者認證等資訊
  • Behavior 使用者行為庫,主要儲存使用者行為,包括使用者的轉發,點贊,評論行為等
  • WeMedia 多媒體庫,主要儲存多媒體人圖文資料統計,賬號資訊,粉絲相關資訊等。
  • Crawlers 爬蟲庫,主要儲存從網路上爬取的文章資訊等。
  • Admin 後臺管理庫,主要儲存後臺管理員的資訊。

4.3 核心資料流轉圖

黑馬專案中的文章採用了多庫設計的方式,以減少高併發情況下核心資料庫表壓力,共計設計為三個庫表:

  • ap_article:APP使用者讀取文章資料和記錄次數
  • cl_news:爬蟲爬得文章資料
  • wm_news:自媒體使用者釋出的文章資料

cl_news和wm_news中的資料稽核通過之後釋出到ap_article中。

4.4 冗餘設計

​ 黑馬頭條專案全部採用邏輯關聯,沒有采用主外來鍵約束。也是方便資料來源冗餘,儘可能少的使用多表關聯查詢。冗餘是為了效率,減少join。單表查詢比關聯查詢速度要快。某個訪問頻繁的欄位可以冗餘存放在兩張表裡,不用關聯了。

​ 如查詢一個訂單表需要查詢該條訂單的使用者名稱稱,就必須join另外使用者表,如果業務表很大,那麼就會查詢的很慢,這個時候我們就可以使用冗餘來解決這個問題,在新建訂單的同時不僅僅需要把使用者ID儲存,同時也需要儲存使用者的名稱,這樣我們在查詢訂單表的時候就不需要去join另外使用者表,也能查詢出該條訂單的使用者名稱稱。這樣的冗餘可以直接的提高查詢效率,單表更快。

4.5 匯入資料庫

當天資料資料夾下:資料庫指令碼

5. 後端工程結構

5.1 後端工程說明

後端工程基於Spring-boot 2.1.5.RELEASE 版本構建,工程父專案為heima-leadnews,並通過繼承方式整合Spring-boot。

【父專案下分4個公共子專案】:

  • heima-leadnews-common : 是整個工程的配置核心,包括所有整合三方框架的配置定義,比如redis、kafka等。除此之外還包括專案每個模組及整個專案的常量定義;

  • heima-leadnews-model :專案中用到的Dto、Pojo、Mapper、Enums定義工程;

  • heima-leadnews-utils : 工程公用工具類專案,包含加密/解密、Date、JSON等工具類;

  • heima-leadnew-apis : 整個專案微服務暴露的介面的定義專案,按每個模組進行子包拆分;

【多個微服務】:

  • heima-leadnews-login:用於實現APP+自媒體端使用者的登入與註冊功能;
  • heima-leadnews-user:用於實現APP端使用者中心的功能,比如我的收藏、我的粉絲等功能;
  • heima-leadnews-article:用於實現APP端文章的獲取與搜尋等功能;還包括頻道、標籤等功能;
  • heima-leadnews-behavior:用於實現APP端各類行為資料的上傳服務;
  • heima-leadnews-webmagic:用於實現文章資料的自動化爬取功能;
  • heima-leadnews-quartz:用於封裝專案中所有的排程計算任務;
  • heima-leadnews-wemedia:用於實現自媒體管理端的功能;
  • heima-leadnews-admin:用於實現後臺管理系統的功能;
  • service-gateway:spring cloud 閘道器
  • service-eureka:spring cloud 註冊中心

5.2 後端通用工程搭建

5.2.1 開發環境說明

專案依賴環境(需提前安裝好):

  • JDK1.8

  • Intellij Idea

  • Tomcat 8.5

  • Git

5.2.2 IDEA開發工具配置

  • 設定本地倉庫,建議使用資料中提供好的倉庫

  • 設定專案編碼格式

5.2.3 後端初始專案匯入

在當天資料中解壓heima-leadnews.zip檔案,拷貝到一個沒有中文和空格的目錄,使用idea開啟即可

6 後端開發-通用說明及開發規範

6.1 後端介面開發規範

6.1.1 開發原則

  • 自頂向下的設計原則:功能應該從表現層分析再到控制層、服務層、持久層逐層設計

  • 自底向上的開發原則:上層需呼叫下層,因此開發應從底層向上層逐層開發

    專案中開發的層次次序參考DB->中介軟體->持久層->服務層->控制層

  • 單一職責的開發原則:類或者方法提供的功能應該單一明確,特別越底層越應單一職責,以便維護

    專案中Mapper方法必須功能單一,引數明確,拒絕兩種以上的持久邏輯使用同一個Mapper方法

  • 依賴倒置的開發原則:上層依賴下層,是依賴下層介面,並不是依賴下層的實現

    專案中每層都是通過介面呼叫Controller->Service->Mapper

6.1.2 開發步驟

  • 明確類定義:明確哪些是重用類,哪些是需要新增的類
  • 明確主鍵規則:確認操作表的ID生成規則,是Mycat主鍵,還是Zk主鍵
  • Mapper實現:查、改、刪時注意是否使用mycat註解確認DN,插入時是否要插入主鍵id
  • Service實現:可用通過時序圖幫助我們梳理實現邏輯
  • ControllerApi定義
  • Controller實現:簡單的Service層呼叫
  • 單元測試

6.1.2 介面版本規範說明

隨著業務的複雜,同一個介面可能出現多個版本,為了方便後期切換和AB測試,需要定義介面的版本號

在某一個微服務下訪問controller的時候在包名下加一個版本號,如下

com.heima.article.controller.v1

在訪問具體的介面方法的url對映的時候也應該加上版本說明,如下:

@RequestMapping("/api/v1/article")

6.2.3 介面通用規範

ID混淆 請求和響應的連續增長的ID需要經過混淆加密
Date數化 請求和響應的的時間欄位,統一轉換成13位時間戳
字元編碼 請求和響應的內容字符集為UTF-8
支援多格式 響應結果支援JSON和XML,可通過Header Accept設定
URL格式 Url為全小寫字元,多個單詞用下劃線分隔
token 請求頭中存放當前使用者的請求token(JWT格式)
t 請求頭中存放當前請求的時間,用於基本的請求時效判斷
md 請求頭中存放當前請求的引數驗簽字符串(查詢串排序MD5加密)
響應格式 響應格式只接受ResponseResult,code碼需定義在AppHttpCodeEnum

6.2 工具類說明

  • IdsUtils工具類

    把數字型別的id做aes加密混淆,比如:在url傳遞的過程中,自增的id會做混淆處理

  • UrlSignUtils工具類

    url簽名工具類

  • AppJwtUtil

    jwt字串生成驗證工具類

  • AppThreadLocalUtils

    當前請求使用者資訊操作類

6.3 介面通用請求和響應

dto(Data Transfer Object):資料傳輸物件,用於展示層與服務層之間的資料傳輸物件

6.3.1 通用的響應物件:

不分頁:com.heima.model.common.dtos.ResponseResult

/**
 * 通用的結果返回類
 * @param <T>
 */
public class ResponseResult<T> implements Serializable {

    private String host;

    private Integer code;

    private String errorMessage;

    private T data;

    public ResponseResult() {
        this.code = 200;
    }

    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.errorMessage = msg;
        this.data = data;
    }

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.errorMessage = msg;
    }

    public static ResponseResult errorResult(int code, String msg) {
        ResponseResult result = new ResponseResult();
        return result.error(code, msg);
    }

    public static ResponseResult okResult(int code, String msg) {
        ResponseResult result = new ResponseResult();
        return result.ok(code, null, msg);
    }

    public static ResponseResult okResult(Object data) {
        ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS,AppHttpCodeEnum.SUCCESS.getErrorMessage());
        if(data!=null) {
            result.setData(data);
        }
        return result;
    }

    public static ResponseResult errorResult(AppHttpCodeEnum enums){
        return setAppHttpCodeEnum(enums,enums.getErrorMessage());
    }

    public static ResponseResult errorResult(AppHttpCodeEnum enums,String errorMessage){
        return setAppHttpCodeEnum(enums,errorMessage);
    }

    public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
        return okResult(enums.getCode(),enums.getErrorMessage());
    }

    private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums,String errorMessage){
        return okResult(enums.getCode(),errorMessage);
    }

    public ResponseResult<?> error(Integer code, String msg) {
        this.code = code;
        this.errorMessage = msg;
        return this;
    }

    public ResponseResult<?> ok(Integer code, T data) {
        this.code = code;
        this.data = data;
        return this;
    }

    public ResponseResult<?> ok(Integer code, T data, String msg) {
        this.code = code;
        this.data = data;
        this.errorMessage = msg;
        return this;
    }

    public ResponseResult<?> ok(T data) {
        this.data = data;
        return this;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }
}

分頁通用返回:com.heima.model.common.dtos.PageResponseResult

public class PageResponseResult extends ResponseResult {
    private Integer currentPage;
    private Integer size;
    private Integer total;

    public PageResponseResult(Integer currentPage, Integer size, Integer total) {
        this.currentPage = currentPage;
        this.size = size;
        this.total = total;
    }

    public int getCurrentPage() {
        return currentPage;
    }

    public void setCurrentPage(int currentPage) {
        this.currentPage = currentPage;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public int getTotal() {
        return total;
    }

    public void setTotal(int total) {
        this.total = total;
    }
}

6.3.2 通用的請求dtos

com.heima.model.common.dtos.PageRequestDto

@Data
@Slf4j
public class PageRequestDto {

    protected Integer size;
    protected Integer page;

    public void checkParam() {
        if (this.page == null || this.page < 0) {
            setPage(1);
        }
        if (this.size == null || this.size < 0 || this.size > 100) {
            setSize(10);
        }
    }
}

6.3.3 通用的異常列舉

com.heima.model.common.enums.AppHttpCodeEnum

public enum AppHttpCodeEnum {

    // 成功段0
    SUCCESS(0,"操作成功"),
    // 登入段1~50
    NEED_LOGIN(1,"需要登入後操作"),
    LOGIN_PASSWORD_ERROR(2,"密碼錯誤"),
    // TOKEN50~100
    TOKEN_INVALID(50,"無效的TOKEN"),
    TOKEN_EXPIRE(51,"TOKEN已過期"),
    TOKEN_REQUIRE(52,"TOKEN是必須的"),
    // SIGN驗籤 100~120
    SIGN_INVALID(100,"無效的SIGN"),
    SIG_TIMEOUT(101,"SIGN已過期"),
    // 引數錯誤 500~1000
    PARAM_REQUIRE(500,"缺少引數"),
    PARAM_INVALID(501,"無效引數"),
    PARAM_IMAGE_FORMAT_ERROR(502,"圖片格式有誤"),
    SERVER_ERROR(503,"伺服器內部錯誤"),
    // 資料錯誤 1000~2000
    DATA_EXIST(1000,"資料已經存在"),
    AP_USER_DATA_NOT_EXIST(1001,"ApUser資料不存在"),
    DATA_NOT_EXIST(1002,"資料不存在"),
    // 資料錯誤 3000~3500
    NO_OPERATOR_AUTH(3000,"無許可權操作");

    int code;
    String errorMessage;

    AppHttpCodeEnum(int code, String errorMessage){
        this.code = code;
        this.errorMessage = errorMessage;
    }

    public int getCode() {
        return code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }
}

6.4 jackson封裝解決欄位序列化及反序列化

主要針對一些資料進行過濾設定,使用jackson來實現,比如一些自增的id值或者一些混淆的屬性,在網際網路傳輸的過程中最好不要輕易暴露在外面,這樣安全性就比較低,應該對這些自增的值進行加密混淆。

日期處理:在網路上進行傳輸的時候不要直接傳輸日期的格式,傳輸的格式為13位時間戳,如果在每個日期欄位都手動設定的話是比較費時費力的,所以也可以做成自動轉換,用的也是jackson來完成

主要的類包括:

  • IdEncrypt 自定義註解 作用在需要混淆的欄位屬性上,用於非id的屬性上 在model包下
  • DateDeserializer 用於處理日期輸入反序列化
  • DateSerializer 用於日期的序列化輸出
  • ConfusionSerializer 用於序列化自增數字的混淆
  • ConfusionDeserializer 用於反序列化自增數字的混淆解密
  • ConfusionSerializerModifier 用於過濾序列化時處理的欄位
  • ConfusionDeserializerModifier 用於過濾反序列化時處理的欄位
  • ConfusionModule 用於註冊模組和修改器
  • InitJacksonConfig 提供自動化配置預設ObjectMapper,讓整個框架自動處理日期和id混淆

6.5 通用環境說明

6.5.1 多環境切換

在每一個微服務的工程中的根目錄下建立三個檔案,方便各個環境的切換

(1)maven_dev.properties

​ 定義開發環境的配置

(2)maven_prod.properties

​ 定義生產環境的配置

(3)maven_test.properties

​ 定義測試環境的配置,開發階段使用這個測試環境

預設載入的環境為test,在打包的過程中也可以指定引數打包 package -P test/prod/dev

具體配置,請檢視父工程下的maven外掛的profiles配置

<profiles>
    <profile>
        <id>dev</id>
        <build>
            <filters>
                <filter>maven_dev.properties</filter>
            </filters>
        </build>
    </profile>
    <profile>
        <id>test</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <build>
            <filters>
                <filter>maven_test.properties</filter>
            </filters>
        </build>
    </profile>
    <profile>
        <id>prod</id>
        <build>
            <filters>
                <filter>maven_prod.properties</filter>
            </filters>
        </build>
    </profile>
</profiles>

6.5.2 mysql的環境配置

在heima-leadnews-common設定配置檔案mysql-core-jdbc.properties

### ======================= 核心資料庫連線配置 =========================
# 資料庫連線字串
mysql.core.jdbc-url=${mysql.core.jdbc.url}
# 資料庫連線名稱
mysql.core.jdbc-user-name=${mysql.core.jdbc.username}
# 資料庫連線密碼,密碼需要反轉
mysql.core.jdbc-password=${mysql.core.jdbc.password}
# 資料庫連線驅動
mysql.core.jdbc-driver=${mysql.core.jdbc.driver}
# mybatis mapper.xml存放在classpath下的根資料夾名稱
mysql.core.root-mapper=${mysql.core.root.mapper}
# mybatis pojo物件別名掃描包
mysql.core.aliases-package=${mysql.core.aliases.package}
# 事務掃描包自動代理掃描包
mysql.core.tx-scan-package=${mysql.core.tx.scan.package}

這裡面的內容統統都是從maven_test.properties讀取

mysql.core.jdbc.url=jdbc:mysql://localhost:3306/heima-leadnews?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
mysql.core.jdbc.username=root
mysql.core.jdbc.password=toor

mysql.core.jdbc.driver=com.mysql.jdbc.Driver
mysql.core.root.mapper=mappers
mysql.core.aliases.package=com.heima.model.**
mysql.core.tx.scan.package=execution(* com.heima..service.*.*(..))

自動化配置核心資料庫的連線配置com.heima.common.mysql.core.MysqlCoreConfig

/**
 * 自動化配置核心資料庫的連線配置
 */
@Setter
@Getter
@Configuration
@ConfigurationProperties(prefix = "mysql.core")
@PropertySource("classpath:mysql-core-jdbc.properties")
@MapperScan(basePackages = "com.heima.model.mappers", sqlSessionFactoryRef = "mysqlCoreSqlSessionFactory")
public class MysqlCoreConfig {
    String jdbcUrl;
    String jdbcUserName;
    String jdbcPassword;
    String jdbcDriver;
    String rootMapper;//mapper檔案在classpath下存放的根路徑
    String aliasesPackage;//別名包

    /**
     * 這是最快的資料庫連線池
     *
     * @return
     */
    @Bean
    public DataSource mysqlCoreDataSource() {
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setUsername(this.getJdbcUserName());
        hikariDataSource.setPassword(this.getRealPassword());
        hikariDataSource.setJdbcUrl(this.getJdbcUrl());
        //最大連線數
        hikariDataSource.setMaximumPoolSize(50);
        //最小連線數
        hikariDataSource.setMinimumIdle(5);
        hikariDataSource.setDriverClassName(this.getJdbcDriver());
        return hikariDataSource;
    }

    /**
     * 這是Mybatis的Session
     *
     * @return
     * @throws IOException
     */
    @Bean
    public SqlSessionFactoryBean mysqlCoreSqlSessionFactory(@Qualifier("mysqlCoreDataSource") DataSource mysqlCoreDataSource) throws IOException {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        //建立sqlSessionFactory工廠物件
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        //資料來源
        sessionFactory.setDataSource(mysqlCoreDataSource);
        //mapper檔案的路徑
        sessionFactory.setMapperLocations(resolver.getResources(this.getMapperFilePath()));
        //別名
        sessionFactory.setTypeAliasesPackage(this.getAliasesPackage());
        //開啟自動駝峰標識轉換
        org.apache.ibatis.session.Configuration mybatisConf = new org.apache.ibatis.session.Configuration();
        mybatisConf.setMapUnderscoreToCamelCase(true);
        sessionFactory.setConfiguration(mybatisConf);
        return sessionFactory;
    }

    /**
     * 密碼反轉,簡單示意密碼在配置檔案中的加密處理
     *
     * @return
     */
    public String getRealPassword() {
        return StringUtils.reverse(this.getJdbcPassword());
    }

    /**
     * 拼接Mapper.xml檔案的存放路徑
     *
     * @return
     */
    public String getMapperFilePath() {
        return new StringBuffer().append("classpath:").append(this.getRootMapper()).append("/**/*.xml").toString();
    }


}

通用事務管理配置類com.heima.common.mysql.core.TransactionConfig

@Setter
@Getter
@Aspect
@EnableAspectJAutoProxy
@EnableTransactionManagement
@Configuration
@ConfigurationProperties(prefix="mysql.core")
@PropertySource("classpath:mysql-core-jdbc.properties")
public class TransactionConfig {

    String txScanPackage;

    /**
     * 初始化事務管理器
     * @param dataSource
     * @return
     */
    @Bean
    public DataSourceTransactionManager mysqlCoreDataSourceTransactionManager(@Qualifier("mysqlCoreDataSource") DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }

    /**
     * 設定事務攔截器
     * @param dataSourceTransactionManager
     * @return
     */
    @Bean
    public TransactionInterceptor mysqlCoreDataSourceTxAdvice(@Qualifier("mysqlCoreDataSourceTransactionManager") DataSourceTransactionManager dataSourceTransactionManager) {
        // 預設事務
        DefaultTransactionAttribute defAttr = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED);
        // 查詢只讀事務
        DefaultTransactionAttribute queryAttr = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED);
        queryAttr.setReadOnly(true);
        // 設定攔截的方法
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        source.addTransactionalMethod("save*", defAttr);
        source.addTransactionalMethod("insert*", defAttr);
        source.addTransactionalMethod("delete*", defAttr);
        source.addTransactionalMethod("update*", defAttr);
        source.addTransactionalMethod("exec*", defAttr);
        source.addTransactionalMethod("set*", defAttr);
        source.addTransactionalMethod("add*", defAttr);
        source.addTransactionalMethod("get*", queryAttr);
        source.addTransactionalMethod("query*", queryAttr);
        source.addTransactionalMethod("find*", queryAttr);
        source.addTransactionalMethod("list*", queryAttr);
        source.addTransactionalMethod("count*", queryAttr);
        source.addTransactionalMethod("is*", queryAttr);

        return new TransactionInterceptor(dataSourceTransactionManager, source);
    }

    @Bean
    public Advisor txAdviceAdvisor(@Qualifier("mysqlCoreDataSourceTxAdvice") TransactionInterceptor mysqlCoreDataSourceTxAdvice) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(txScanPackage);
        return new DefaultPointcutAdvisor(pointcut, mysqlCoreDataSourceTxAdvice);
    }

}

6.4.3 實體類及mapper

所有實體類都是按業務模板劃分,mapper介面單獨在一個包下,如下圖

com.heima.model.mappers 定義mapper介面類

resources/mapper mapper對映檔案的定義

7 app端-文章列表後端開發

7.1 文章列表需求分析

文章首頁為使用者進入應用之後的第一個頁面,在這裡我我們需要根據使用者是否登入的狀態載入不同的文章資料;使用者登入則根據其選擇的頻道進行載入,否則根據系統預設頻道推薦,流程圖如下;在首頁我們載入資料之後需要考慮使用者在首頁的其他動作的相應,如:使用者上拉重新整理、使用者下拉重新整理、使用者進入文章詳情等使用者動作

相關表結構分析

ap_article文章資訊表

文章資訊表,儲存已釋出的文章

欄位名稱 型別 說明
id int(11) 主鍵
title varchar(50) 標題
author_id int(11) 文章作者的ID
author_name varchar(20) 作者暱稱
channel_id int(10) 文章所屬頻道ID
channel_name varchar(10) 頻道名稱
layout tinyint(1) 文章佈局 0 無圖文章 1 單圖文章 2 多圖文章
flag tinyint(3) 文章標記 0 普通文章 1 熱點文章 2 置頂文章 3 精品文章 4 大V 文章
images varchar(1000) 文章圖片 多張逗號分隔
labels varchar(500) 文章標籤最多3個 逗號分隔
likes int(5) 點贊數量
collection int(5) 收藏數量
comment int(5) 評論數量
views int(5) 閱讀數量
province_id int(11) 省市
city_id int(11) 市區
county_id int(11) 區縣
created_time datetime 建立時間
publish_time datetime 釋出時間
sync_status tinyint(1) 同步狀態
origin tinyint(1) 來源

ap_user_article_list APP使用者文章列表

欄位名稱 型別 說明
id int(11) 主鍵
user_id int(11) 使用者ID
channel_id int(11) 頻道ID
article_id int(11) 文章ID
is_show tinyint(1) 是否展示
recommend_time datetime 推薦時間
is_read tinyint(1) 是否閱讀
strategy_id int(5) 推薦演算法

ap_show_behaviorAPP文章展現行為表

欄位 型別 描述
id int(11) 主鍵
entry_id int(11) 實體ID
article_id int(11) 文章ID
is_click tinyint(1) 是否點選
show_time datetime 文章載入時間
created_time datetime 登入時間

ap_behavior_entry app行為實體表

APP行為實體表,一個行為實體可能是使用者或者裝置,或者其它

欄位 型別 描述
id int(11) 主鍵
type tinyint(1) 實體型別 0終端裝置 1使用者
entry_id int(11) 實體ID
created_time datetime 建立時間
burst varchar(40) 分片

7.2 文章列表相關介面定義

7.2.1 介面定義分析

由需求分析可知使用者在首頁的是可能觸發的行為有載入文章列表,重新整理(上拉重新整理、下拉重新整理)等動作,這也就是我們後端需要對應的幾個資料介面,其中還包含一個隱含的使用者行為介面使用者記錄使用者是否閱讀某一篇文章的行為介面,則我們可以分析出後端需要的介面有:

(1)load介面

load介面,分兩種情況,一個是登入,一個是未登入,載入多條資料(有條數的限制,size),使用者可以選擇頻道進行資料的切換

  • 登入,從後臺獲取使用者資訊,作為條件查詢

  • 未登入,直接載入預設資料即可。

(2)load_more介面 && load_new 介面

當用戶進行重新整理是,在我們的系統中定義了兩種操作,第一種是上拉重新整理也就是load_more,第二種是下拉重新整理load_new介面,這兩個介面的區別在於載入的內容的時間不同

使用者進入系統的時間TimeA瀏覽了一會首次載入的資料之後到了TimeB時間,這時候如果使用者繼續上拉看後面的內容則我們呼叫load_more介面並把TimeB時間傳遞到後端,後端根據TimeB時間查詢當前時間之前釋出的內容;

使用者下拉重新整理則說明使用者需要當前最新的內容則呼叫load_new介面,將TimeA時間傳到後端後端查詢TimeA時間之後釋出的內容。其請求引數介面設計和load介面相同。

(4)behavior 行為介面

記錄使用者操作行為

7.3 專案開發

在原有的工程中建立一個普通的maven工程模組,選擇其作為heima-leadnews作為父工程,並給當前模組命名為heima-leadnews-article

配置我們需要的jar的maven座標,以及我們專案中模組的專案依賴 注意我們的資料庫相關的實體以及Mapper介面和配置檔案都是存放在Model 模組。

7.3.1 定義pom檔案中的依賴資訊

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>heima-leadnews</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>heima-leadnews-article</artifactId>

    <dependencies>
        <!-- 引入依賴模組 -->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>heima-leadnews-model</artifactId>
        </dependency>
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>heima-leadnews-common</artifactId>
        </dependency>
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>heima-leadnews-apis</artifactId>
        </dependency>
        <!-- Spring boot starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- 排除預設的logback日誌,使用log4j-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>ch.qos.logback</groupId>
                    <artifactId>logback-access</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-cbor</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>
    </dependencies>
</project>

7.3.2 Article模組工程結構

注意這裡可以只建立基礎的包結構即可,後續會講解每個具體的類的作用以及實現;此處可能你又疑問為什麼我們的控制器加了個v1,這裡我們的做法是為了相容不同的終端版本所設立的版本號

專案根路徑新增檔案:maven_dev.properties

# log4j
log.level=DEBUG
log.pattern=%d{DEFAULT}^|%sn^|%level^|%t^|%c^|%M^|%msg%n

專案根路徑新增檔案:maven_prod.properties

# log4j
log.level=DEBUG
log.pattern=%d{DEFAULT}^|%sn^|%level^|%t^|%c^|%M^|%msg%n

專案根路徑新增檔案:maven_test.properties

#log4j
log.level=DEBUG
log.pattern=%d{DEFAULT}^|%sn^|%level^|%t^|%c^|%M^|%msg%n

在resource目錄下建立application.properties和log4j2.xml

application.properties

server.port=${port.article}
spring.application.name=${sn.article}

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <properties>
        <property name="CONSOLE_PATTERN">${log.pattern}</property>
        <property name="FILE_NAME">${project.build.finalName}</property>
    </properties>
    <!--先定義所有的appender-->
    <appenders>
        <!--這個輸出控制檯的配置-->
        <console name="Console" target="SYSTEM_OUT">
            <!--輸出日誌的格式-->
            <PatternLayout pattern="${CONSOLE_PATTERN}"/>
        </console>
        <!-- 這個會打印出所有的info及以下級別的資訊,每次大小超過size,則這size大小的日誌會自動存入按年份-月份建立的資料夾下面並進行壓縮,作為存檔-->
        <RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/${FILE_NAME}.log"
                     filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout pattern="${CONSOLE_PATTERN}"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>
    </appenders>
    <loggers>
        <!--過濾掉spring和mybatis的一些無用的DEBUG資訊-->
        <logger name="org.springframework" level="INFO"></logger>
        <logger name="org.mybatis" level="INFO"></logger>
        <logger name="org.apache.http" level="INFO"></logger>
        <logger name="org.apache.kafka" level="INFO"></logger>
        <logger name="com.netflix.discovery" level="INFO"></logger>
        <logger name="org.hibernate" level="INFO"></logger>
        <root level="${log.level}">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo"/>
        </root>
    </loggers>
</configuration>

mysql初始化掃描配置類com.heima.article.config.MysqlConfig

@Configuration
@ComponentScan("com.heima.common.mysql.core")
public class MysqlConfig {

}

7.3.3 article服務功能開發

(0)定義dto

@Data
public class ArticleHomeDto {

    // 省市
    Integer provinceId;
    // 市區
    Integer cityId;
    // 區縣
    Integer countyId;
    // 最大時間
    Date maxBehotTime;
    // 最小時間
    Date minBehotTime;
    // 分頁size
    Integer size;
    // 資料範圍,比如頻道ID
    String tag;

}

(1)介面定義,在apis模組中我們建立包com.heima.article.apis並定義介面ArticleHomeControllerApi

/**
 * 首頁文章
 */
public interface ArticleHomeControllerApi {
    /**
     * 載入首頁文章
     * @param dto 封裝引數物件
     * @return 文章列表資料
     */
    ResponseResult load(ArticleHomeDto dto);

    /**
     * 載入更多
     * @param dto 封裝引數物件
     * @return 文章列表資料
     */
    ResponseResult loadMore(ArticleHomeDto dto);

    /**
     * 載入最新的資料
     * @param dto 封裝引數物件
     * @return 文章列表
     */
    ResponseResult loadNew(ArticleHomeDto dto);

}

(2)在定義完資料介面之後,我們需要做的就是去article模組定義我們的控制器, 如果你是拷貝的下面程式碼你可能發現你的程式碼中ArticleIndexService沒有定義,報錯了這裡不用慌我們後面就是service層的編寫,後面service到dao層也是同樣的

@RestController
@RequestMapping("/api/v1/article")
public class ArticleHomeController implements ArticleHomeControllerApi {

    @Autowired
    private AppArticleService appArticleService;



    @Override
    @GetMapping("/load")
    public ResponseResult load(ArticleHomeDto dto) {
        return appArticleService.load( ArticleConstans.LOADTYPE_LOAD_MORE, dto);
    }

    @Override
    @GetMapping("/loadmore")
    public ResponseResult loadMore(ArticleHomeDto dto) {
        return appArticleService.load( ArticleConstans.LOADTYPE_LOAD_MORE, dto);
    }

    @Override
    @GetMapping("/loadnew")
    public ResponseResult loadNew(ArticleHomeDto dto) {
        return appArticleService.load( ArticleConstans.LOADTYPE_LOAD_NEW, dto);
    }

}

定義常量com.heima.common.article.constans.ArticleConstans

public class ArticleConstans{
    public static final Short LOADTYPE_LOAD_MORE = 1;
    public static final Short LOADTYPE_LOAD_NEW = 2;
    public static final String DEFAULT_TAG = "__all__"
    
}

(3)文章service介面定義

public interface AppArticleService {

    /**
     *
     * @param type 1 載入更多  2 載入更新
     * @param dto 封裝資料
     * @return 資料列表
     */
    public ResponseResult load(Short type, ArticleHomeDto dto);
}

(4)AppArticleServiceImpl實現類

@Service
public class AppArticleServiceImpl implements AppArticleService {

    // 單頁最大載入的數字
    private final  static short MAX_PAGE_SIZE = 50;


    @Autowired
    private ApArticleMapper apArticleMapper;
    @Autowired
    private ApUserArticleListMapper apUserArticleListMapper;


    /**
     *
     * @param time 時間節點
     * @param type 1 載入更多  2 載入更新
     * @param size 每次返回資料量
     * @return 資料列表
     */
    public ResponseResult load(Short type, ArticleHomeDto dto) {
        ApUser user = AppThreadLocalUtils.getUser();
        Integer size = dto.getSize();
        String tag = dto.getTag();
        // 分頁引數校驗
        if (size == null || size <= 0) {
            size = 20;
        }
        size = Math.min(size,MAX_PAGE_SIZE);
        dto.setSize(size);
        //  型別引數校驗
        if (!type.equals(ArticleConstans.LOADTYPE_LOAD_MORE) && !type.equals(ArticleConstans.LOADTYPE_LOAD_NEW))
            type = ArticleConstans.LOADTYPE_LOAD_MORE;
        // 文章頻道引數驗證
        if (StringUtils.isEmpty(tag)) {
            dto.setTag(ArticleConstans.DEFAULT_TAG);
        }
        // 最大時間處理
        if(dto.getMaxBehotTime()==null){
            dto.setMaxBehotTime(new Date());
        }
        // 最小時間處理
        if(dto.getMinBehotTime()==null){
            dto.setMinBehotTime(new Date());
        }
        // 資料載入
        if(user!=null){
            return ResponseResult.okResult(getUserArticle(user,dto,type));
        }else{
            return ResponseResult.okResult(getDefaultArticle(dto,type));
        }
    }

    /**
     * 先從使用者的推薦表中查詢文章,如果沒有再從大文章列表中獲取
     * @param user
     * @param dto
     * @param type
     * @return
     */
    private List<ApArticle> getUserArticle(ApUser user,ArticleHomeDto dto,Short type){
        List<ApUserArticleList> list = apUserArticleListMapper.loadArticleIdListByUser(user,dto,type);
        if(!list.isEmpty()){
            List<ApArticle> temp = apArticleMapper.loadArticleListByIdList(list);
            return temp;
        }else{
            return getDefaultArticle(dto,type);
        }
    }

    /**
     * 從預設的大文章列表中獲取文章
     * @param dto
     * @param type
     * @return
     */
    private List<ApArticle> getDefaultArticle(ArticleHomeDto dto,Short type){
        return apArticleMapper.loadArticleListByLocation(dto,type);
    }

}

(5)ApArticleMapper

public interface ApArticleMapper {


    /**
     * 照使用者地理位置,載入文章
     * @param dto   引數封裝物件
     * @param type  載入方向
     * @return
     */
    List<ApArticle> loadArticleListByLocation(@Param("dto") ArticleHomeDto dto, @Param("type") short type);

    /**
     * 依據文章IDS來獲取文章詳細內容
     * @param list   文章ID
     * @return
     */
    List<ApArticle> loadArticleListByIdList(@Param("list") List<ApUserArticleList> list);


}

(6)ApArticleMapper 對應xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heima.model.mappers.app.ApArticleMapper">
    <resultMap id="resultMap" type="com.heima.model.article.pojos.ApArticle">
        <id column="id" property="id"/>
        <result column="title" property="title"/>
        <result column="author_id" property="authorId"/>
        <result column="author_name" property="authorName"/>
        <result column="channel_id" property="channelId"/>
        <result column="channel_name" property="channelName"/>
        <result column="layout" property="layout"/>
        <result column="flag" property="flag"/>
        <result column="images" property="images"/>
        <result column="labels" property="labels"/>
        <result column="likes" property="likes"/>
        <result column="collection" property="collection"/>
        <result column="comment" property="comment"/>
        <result column="views" property="views"/>
        <result column="province_id" property="provinceId"/>
        <result column="city_id" property="cityId"/>
        <result column="county_id" property="countyId"/>
        <result column="created_time" property="createdTime"/>
        <result column="publish_time" property="publishTime"/>
        <result column="sync_status" property="syncStatus"/>
    </resultMap>
    <sql id="Base_Column_List">
    id, title, author_id, author_name, channel_id, channel_name, layout, flag, images,
    labels, likes, collection, comment, views, province_id, city_id, county_id, created_time, 
    publish_time,sync_status
  </sql>

    <!-- 依據地理位置獲取 -->
    <select id="loadArticleListByLocation" resultMap="resultMap">
        select * from ap_article a
        <where>
            <if test="dto.provinceId!=null">
                and a.province_id=#{dto.provinceId}
            </if>
            <if test="dto.cityId!=null">
                and a.city_id=#{dto.cityId}
            </if>
            <if test="dto.countyId!=null">
                and a.county_id=#{dto.countyId}
            </if>
            <!-- loadmore -->
            <if test="type != null and type == 1">
                and a.publish_time <![CDATA[<]]> #{dto.minBehotTime}
            </if>
            <if test="type != null and type == 2">
                and a.publish_time <![CDATA[>]]> #{dto.maxBehotTime}
            </if>
            <if test="dto.tag != '__all__'">
                and a.channel_id = #{dto.tag}
            </if>
        </where>
        limit #{dto.size}
    </select>

    <!-- 以及文章IDS列表獲取文章資料 -->
    <select id="loadArticleListByIdList" resultMap="resultMap">
        select * from ap_article where id in(
        <trim prefix="" suffixOverrides=",">
            <foreach item="item" collection="list" separator=",">
                #{item.articleId},
            </foreach>
        </trim>
        )
    </select>
</mapper>

(7)ApUserArticleListMapper

package com.heima.article.mysql.core.model.mappers.app;

import com.heima.article.mysql.core.model.dtos.ArticleHomeDto;
import com.heima.article.mysql.core.model.pojos.app.ApUser;
import com.heima.article.mysql.core.model.pojos.app.ApUserArticleList;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface ApUserArticleListMapper {
    /**
     * 按照使用者屬性閱讀習慣,載入文章id
     * @param user  當前登入的使用者
     * @param dto   引數封裝物件
     * @param type  載入方向
     * @return
     */
    List<ApUserArticleList> loadArticleIdListByUser(@Param("user") ApUser user, @Param("dto") ArticleHomeDto dto, @Param("type") short type);

}

(8)ApUserArticleListMapper 對應xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heima.article.mysql.core.model.mappers.app.ApUserArticleListMapper">
<resultMap id="BaseResultMap" type="com.heima.model.user.pojos.ApUserArticleList">
        <id column="id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="channel_id" property="channelId"/>
        <result column="article_id" property="articleId"/>
        <result column="is_show" property="isShow" javaType="java.lang.Boolean" jdbcType="BIT" />
        <result column="recommend_time" property="recommendTime" javaType="java.util.Date" jdbcType="TIMESTAMP" />
        <result column="is_read" property="isRead" javaType="java.lang.Boolean" jdbcType="BIT" />
        <result column="strategy_id" property="strategyId"/>
    </resultMap>
  <sql id="Base_Column_List">

    id, user_id, channel_id, article_id, is_show, recommend_time, is_read, strategy_id
  </sql>
  <select id="loadArticleIdListByUser" parameterType="map" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from ap_user_article_list
    <where>
      user_id=#{user.id} and is_show=0 and is_read=0
      <!-- loadmore -->
      <if test="type != null and type == 1">
          and recommend_time <![CDATA[<]]> #{dto.minBehotTime}
      </if>
      <if test="type != null and type == 2">
          and recommend_time <![CDATA[>]]> #{dto.maxBehotTime}
      </if>
      <if test="dto.tag != '__all__'">
          and channel_id = #{dto.tag}
      </if>
  </where>
    limit #{dto.size}
  </select>
</mapper>

(9)單元測試

/**
 * 測試文章列表相關介面
 */
@SpringBootTest(classes = ArticleJarApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class ArticleTest {

    @Autowired
    private AppArticleService appArticleService;

    /**
     * 測試load
     */
    @Test
    public void testLoad() {
        ApUser apUser = new ApUser();
        apUser.setId(1l);
        AppThreadLocalUtils.setUser(apUser);
        ArticleHomeDto dto = new ArticleHomeDto();
        ResponseResult data = appArticleService.load( ArticleConstans.LOADTYPE_LOAD_MORE, dto);
        System.out.println(data.getData());
    }

}

7.3.4 行為相關功能開發

思路分析

匯入heima-leadnews-behavior工程

dto的定義:

com.heima.model.behavior.dtos.ShowBehaviorDto

@Data
public class ShowBehaviorDto {
    // 裝置ID
    @IdEncrypt
    Integer equipmentId;
    List<ApArticle> articleIds;
}

定義控制器以及控制器介面

(1)介面定義,com.heima.article.apis.BehaviorControllerApi

/**
 * 行為
 */
public interface BehaviorControllerApi {

    ResponseResult saveShowBehavior(ShowBehaviorDto dto);

}

(2)定義控制器:com.heima.behavior.controller.v1.BehaviorController

@RestController
@RequestMapping("/api/v1/behavior")
public class BehaviorController implements BehaviorControllerApi {
    @Autowired
    private AppShowBehaviorService appShowBehaviorService;

    @Override
    @PostMapping("/show_behavior")
    public ResponseResult saveShowBehavior(@RequestBody ShowBehaviorDto dto) {
        return appShowBehaviorService.saveShowBehavior(dto);
    }

}

(3)行為服務層介面:com.heima.behavior.service.AppShowBehaviorService

public interface AppShowBehaviorService {

    /**
     * 儲存行為資料
     * @param dto
     * @return
     */
    public ResponseResult saveShowBehavior(ShowBehaviorDto dto);

}

(4)AppShowBehaviorService 實現

@Service
@SuppressWarnings("all")
public class AppShowBehaviorServiceImpl implements AppShowBehaviorService {

    @Autowired
    private ApShowBehaviorMapper apShowBehaviorMapper;
    @Autowired
    private ApBehaviorEntryMapper apBehaviorEntryMapper;

    @Override
    public ResponseResult saveShowBehavior(ShowBehaviorDto dto){
        ApUser user = AppThreadLocalUtils.getUser();
        // 使用者和裝置不能同時為空
        if(user==null&& (dto.getArticleIds()==null||dto.getArticleIds().isEmpty())){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_REQUIRE);
        }
        Long userId = null;
        if(user!=null){
            userId = user.getId();
        }
        ApBehaviorEntry apBehaviorEntry = apBehaviorEntryMapper.selectByUserIdOrEquipment(userId, dto.getEquipmentId());
        // 行為實體找以及註冊了,邏輯上這裡是必定有值得,除非引數錯誤
        if(apBehaviorEntry==null){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        // 過濾新資料
        Integer[] temp = new Integer[dto.getArticleIds().size()];
        for (int i = 0; i < temp.length; i++) {
            temp[i]=dto.getArticleIds().get(i).getId();
        }
        List<ApShowBehavior> list = apShowBehaviorMapper.selectListByEntryIdAndArticleIds(apBehaviorEntry.getId(), temp);
        List<Integer> stringList = new ArrayList(Arrays.asList(temp));
        if(!list.isEmpty()){
            list.forEach(item->{
                stringList.remove(item.getArticleId());
            });
        }
        // 插入新資料
        if(!stringList.isEmpty()) {
            temp = new Integer[stringList.size()];
            stringList.toArray(temp);
            apShowBehaviorMapper.saveBehaviors(apBehaviorEntry.getId(), temp);
        }
        return ResponseResult.okResult(0);
    }
}

(5)heima-leadnews-model中定義行為mapper介面:com.heima.article.mysql.core.model.mappers.app.ApShowBehaviorMapper

public interface ApShowBehaviorMapper {
    /**
     * 獲取以及存在的使用者資料
     * @param entryId
     * @param articleIds
     * @return
     */
    List<ApShowBehavior> selectListByEntryIdAndArticleIds(@Param("entryId") Integer entryId, @Param("articleIds") Integer[] articleIds);

    /**
     * 儲存使用者展現行為資料
     * @param articleIds  文章IDS
     * @param entryId 實體ID
     */
    void saveBehaviors(@Param("entryId") Integer entryId, @Param("articleIds") Integer[] articleIds);
}

(6)heima-leadnews-model中定義行為mapper檔案:mappers/app/ApShowBehaviorMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.heima.article.mysql.core.model.mappers.app.ApShowBehaviorMapper" >
  <resultMap id="BaseResultMap" type="com.heima.article.mysql.core.model.pojos.app.ApShowBehavior" >
      <id column="id" property="id" />
      <result column="entry_id" property="entryId" />
      <result column="article_id" property="articleId" />
      <result column="is_click" property="isClick"/>
      <result column="show_time" property="showTime" />
      <result column="created_time" property="createdTime" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, entry_id, article, is_click, show_time, created_time
  </sql>


  <!-- 選擇使用者的行為物件,優先按使用者選擇 -->
  <select id="selectListByEntryIdAndArticleIds" resultMap="BaseResultMap" >
    select * from ap_show_behavior a where a.entry_id=#{entryId} and article_id in(
    <foreach item="item" collection="articleIds" separator=",">
      #{item}
    </foreach>
    )
  </select>

  <insert id="saveBehaviors">
  /*!mycat:catlet=io.mycat.route.sequence.BatchInsertSequence */
  insert into ap_show_behavior ( entry_id, article_id,is_click, show_time, created_time) values
    <foreach item="item" collection="articleIds" separator=",">
      (#{entryId}, #{item},0, now(),now())
    </foreach>
  </insert>
</mapper>

(7)ApBehaviorEntryMapper

public interface ApBehaviorEntryMapper {

    ApBehaviorEntry selectByUserIdOrEquipment(@Param("userId") Integer userId, @Param("equipmentId") Integer equipmentId);

}

(8)ApBehaviorEntryMapper 對應對映配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.heima.article.mysql.core.model.mappers.app.ApBehaviorEntryMapper" >
  <resultMap id="BaseResultMap" type="com.heima.article.mysql.core.model.pojos.app.ApBehaviorEntry" >
      <id column="id" property="id" />
      <result column="type" property="type"/>
      <result column="entry_id" property="entryId" />
      <result column="created_time" property="createdTime" />
      <result column="burst" property="burst"/>
  </resultMap>
  <sql id="Base_Column_List" >
    id, type, entry_id, created_time
  </sql>
  <!-- 選擇使用者的行為物件,優先按使用者選擇 -->
  <select id="selectByUserIdOrEquipment" resultMap="BaseResultMap" >
    select * from ap_behavior_entry a
    <where>
      <if test="userId!=null">
        and a.entry_id=#{userId} and type=1
      </if>
      <if test="userId==null and equipmentId!=null">
        and a.entry_id=#{equipmentId} and type=0
      </if>
    </where>
    limit 1
  </select>
</mapper>


行為介面測試

/**
 * 測試文章列表相關介面
 */
@SpringBootTest(classes = BehaviorJarApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class BehaviorTest {


    @Autowired
    private AppShowBehaviorService showBehaviorService;


    @Test
    public void testSaveBehavior() {
        ApUser apUser = new ApUser();
        apUser.setId(1l);
        AppThreadLocalUtils.setUser(apUser);
        ShowBehaviorDto dto = new ShowBehaviorDto();
        List<ApArticle> articles = new ArrayList<>();
        ApArticle apArticle = new ApArticle();
        apArticle.setId(1);
        articles.add(apArticle);
        showBehaviorService.saveShowBehavior(dto);
        //articleIndexService.saveBehaviors(data);
    }
}