SpringBoot技術棧搭建我的博客【後臺開發】

前言:在以前,咱們已經完成了項目的基本準備,那麼就能夠開始後臺開發了,忽然又想到一個問題,就是準備的時候只是設計了前臺的RESTful APIs,可是後臺管理咱們一樣也是須要API的,那麼就在這一篇裏面一塊兒實現了吧...html

一些設計上的調整

在查了一些資料和吸取了一些評論給出良好的建議以後,我以爲有必要對一些設計進行一些調整:前端

  • 1)數據庫:命名應該更加規範,好比表示分類最好用category而不是sort,表示評論最好用comment而不是message;
  • 2)RESful APIs:在準備着手開始寫後臺的時候就已經發現,原本想的是凡是以/api開頭的都是暴露出來給前端用的,凡是以/admin開頭的都是給後臺使用的地址,可是意外的沒有設計後天的API也把一些刪除命令暴露給了前端,這就很差了從新設計設計;
  • 3)命名規範的問題:由於使用MyBatis逆向工程自動生成的時候,配置了一個useActualColumnNames使用表真正名稱的東西,因此整得來生成POJO類基礎字段有下劃線,看着着實有點不爽,把它給幹掉幹掉...;

數據庫調整

把字段規範了一下,而且刪除了分類下是否有效的字段(感受這種不常常變換的字段留着也沒啥用乾脆幹掉..),因此調整爲了下面這個樣子(調整字段已標紅):java

而後從新使用生成器自動生成對應的文件,注意記得修改generatorConfig.xml文件中對應的數據庫名稱;mysql

建立和修改時間的字段設置

經過查資料發現其實咱們能夠經過直接設置數據庫來自動更新咱們的modified_by字段,而且能夠像設置初始值那樣給create_by和modified_by兩個字段以當前時間戳設置默認值,這裏具體以tbl_article_info這張表爲例:android

CREATE TABLE `tbl_article_info` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '文章標題',
  `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章簡介,默認100個漢字之內',
  `is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '文章是否置頂,0爲否,1爲是',
  `traffic` int(10) NOT NULL DEFAULT '0' COMMENT '文章訪問量',
  `create_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `modified_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改日期',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

咱們經過設置DEFAULTCURRENT_TIMESTAMP,而後給modified_by字段多添加了一句ON UPDATE CURRENT_TIMESTAMP,這樣它就會在更新的時候將該字段的值設置爲更新時間,這樣咱們就不用在後臺關心這兩個值了,也少寫了一些代碼(實際上是寫代碼的時候發現能夠這樣偷懶..hhh...);git

RESTful APIs從新設計

咱們須要把一些不可以暴露給前臺的API收回,而後再設計一下後臺的API,搗鼓了一下,最後大概是這個樣子了:github

後臺Restful APIs:web

前臺開放RESful APIs:spring

這些API只是用來和前端交互的接口,另一些關於日誌啊之類的東西就直接在後臺寫就好了,OK,這樣就爽多了,能夠開始着手寫代碼了;sql

基本配置

隨着配置內容的增多,我逐漸的想要放棄.yml的配置文件,主要的一點是這東西很差對內容進行分類(下圖是簡單配置了一些基本文件後的.yml和.properties文件的對比)..

最後仍是用回.properties文件吧,不分類仍是有點難受

編碼設置

咱們首先須要解決的是中文亂碼的問題,對應GET請求,咱們能夠經過修改Tomcat的配置文件【server.xml】來把它默認的編碼格式改成UTF-8,而對於POST請求,咱們須要統一配置一個攔截器同樣的東西把請求的編碼統一改爲UTF-8:

## ——————————編碼設置——————————
spring.http.encoding.charset=UTF-8
spring.http.encoding.force=true
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8

可是這樣設置以後,在後面的使用當中仍是會發生提交表單時中文亂碼的問題,在網上搜索了一下找到了解決方法,新建一個【config】包建立下面這樣一個配置類:

@Configuration
public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {
    @Bean
    public HttpMessageConverter<String> responseBodyConverter() {
        StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
        return converter;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
    }
}

數據庫及鏈接池配置

決定這一次試試Druid的監控功能,因此給一下數據庫的配置:

## ——————————數據庫訪問配置——————————
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=UTF-8
spring.datasource.username = root
spring.datasource.password = 123456

# 下面爲鏈接池的補充設置,應用到上面全部數據源中
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
# 配置獲取鏈接等待超時的時間
spring.datasource.druid.max-wait=60000
# 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一個鏈接在池中最小生存的時間,單位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
# 打開PSCache,而且指定每一個鏈接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
# 配置監控統計攔截的filters,去掉後監控界面sql沒法統計,'wall'用於防火牆
spring.datasource.druid.filters=stat,wall,log4j

日誌配置

在SpringBoot中其實已經使用了Logback來做爲默認的日誌框架,這是log4j做者推出的新一代日誌框架,它效率更高、可以適應諸多的運行環境,同時自然支持SLF4J,在SpringBoot中咱們無需再添加額外的依賴就能使用,這是由於在spring-boot-starter-web包中已經有了該依賴了,因此咱們只須要進行配置使用就行了

第一步:建立logback-spring.xml

當項目跑起來的時候,咱們不可能還去看控制檯的輸出信息吧,因此咱們須要把日誌寫到文件裏面,在網上找到一個例子(連接:http://tengj.top/2017/04/05/springboot7/)

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <contextName>logback</contextName>
    <!--本身定義一個log.path用於說明日誌的輸出目錄-->
    <property name="log.path" value="/log/wmyskxz/"/>
    <!--輸出到控制檯-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
             <level>ERROR</level>
         </filter>-->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--輸出到文件-->
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/logback.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>

    <!-- logback爲java中的包 -->
    <logger name="cn.wmyskxz.blog.controller"/>
</configuration>

在Spring Boot中你只要按照規則組織文件名,就可以使得配置文件可以被正確加載,而且官方推薦優先使用帶有-spring的文件名做爲日誌的配置(如上面使用的logback-spring.xml,而不是logback.xml),知足這樣的命名規範而且保證文件在src/main/resources下就行了;

第二步:重啓項目檢查是否成功

咱們定義的目錄位置爲/log/wmyskxz/,可是在項目的根目錄下並無發現這樣的目錄,反而是在當前盤符的根目錄..不是很懂這個規則..總之是成功了的..

打開是密密麻麻一堆跟控制檯同樣的【info】級別的信息,由於這個系統自己就比較簡單,因此就沒有必要去搞什麼文本切割之類的東西了,ok..日誌算是配置完成;

實際測試了一下,上線以後確定須要調整輸出級別的,否則日誌文件就會特別大...

攔截器配置

咱們須要對地址進行攔截,對全部的/admin開頭的地址請求進行攔截,由於這是後臺管理的默認訪問地址開頭,這是必須進行驗證以後才能訪問的地址,正如上面的RESTful APIs,這裏包含了一些增長/刪除/更改/編輯一類的操做,而通通這些操做都是不可以開放給用戶的操做,因此咱們須要對這些地址進行攔截:

第一步:建立User實體類

作驗證仍是須要添加session,否則很差弄,因此咱們仍是得建立一個常規的實體:

public class User {
    private String username;
    private String password;

    /* getter and setter */
}

第二步:建立攔截器並繼承HandlerInterceptor接口

在【interceptor】包下新建一個【BackInterceptor】類並繼承HandlerInterceptor接口:

public class BackInterceptor implements HandlerInterceptor {

    private static String username = "wmyskxz";
    private static String password = "123456";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        boolean flag = true;
        User user = (User) request.getSession().getAttribute("user");
        if (null == user) {
            flag = false;
        } else {
            // 對用戶帳號進行驗證,是否正確
            if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
                flag = true;
            } else {
                flag = false;
            }
        }
        return flag;
    }
}

在攔截器中,咱們從session中取出了user,並判斷是否符合要求,這裏咱們直接寫死了(並無更改密碼的需求,但須要加密),並且咱們並沒有作任何的跳轉操做,緣由很簡單,根本就不須要跳轉,由於訪問後臺的用戶只有我一我的,因此只須要我知道正確的登陸地址就能夠了...

第三步:在配置類中複寫addInterceptors方法

剛纔咱們在設置編碼的時候本身建立了一個繼承自WebMvcConfigurerAdapter的設置類,咱們須要複寫其中的addInterceptors方法來爲咱們的攔截器添加配置:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用於添加攔截規則
    // excludePathPatterns 用戶排除攔截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    super.addInterceptors(registry);
}
  • 說明:這個方法也很簡單,經過在addPathPatterns中添加攔截規則(這裏設置攔截/admin開頭的全部地址),並經過excludePathPatterns來排除攔截的地址(這裏爲/toLogin,即登陸地址,到時候我能夠弄得複雜隱蔽一點兒)

第四步:配置登陸頁面

之前咱們在寫Spring MVC的時候,若是須要訪問一個頁面,必需要在Controller中添加一個方法跳轉到相應的頁面才能夠,可是在SpringBoot中增長了更加方便快捷的方法:

/**
 * 之前要訪問一個頁面須要先建立個Controller控制類,在寫方法跳轉到頁面
 * 在這裏配置後就不須要那麼麻煩了,直接訪問http://localhost:8080/toLogin就跳轉到login.html頁面了
 *
 * @param registry
 */
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/admin/login").setViewName("login.html");
    super.addViewControllers(registry);
}
  • 注意:login.html記得要放在【templates】下才會生效哦...(我試過使用login綁定視圖名不成功,只能寫全了...)

訪問日誌記錄

上面咱們設置了訪問限制的攔截器,對後臺訪問進行了限制,這是攔截器的好處,咱們一樣也使用攔截器對於訪問數量進行一個統計

第一步:編寫前臺訪問攔截器

對照着數據庫的設計,咱們須要保存的信息都從request對象中去獲取,而後保存到數據庫中便可,代碼也很簡單:

public class ForeInterceptor implements HandlerInterceptor {

    @Autowired
    SysService sysService;

    private SysLog sysLog = new SysLog();
    private SysView sysView = new SysView();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 訪問者的IP
        String ip = request.getRemoteAddr();
        // 訪問地址
        String url = request.getRequestURL().toString();
        //獲得用戶的瀏覽器名
        String userbrowser = BrowserUtil.getOsAndBrowserInfo(request);

        // 給SysLog增長字段
        sysLog.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysLog.setOperateBy(StringUtils.isEmpty(userbrowser) ? "獲取瀏覽器名失敗" : userbrowser);
        sysLog.setOperateUrl(StringUtils.isEmpty(url) ? "獲取URL失敗" : url);

        // 增長訪問量
        sysView.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysService.addView(sysView);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        // 保存日誌信息
        sysLog.setRemark(method.getName());
        sysService.addLog(sysLog);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
  • 注意:可是須要注意的是測試的時候別把攔截器開了(主要是postHandle方法中中沒法強轉handler),否則不方便測試...

BrowserUtil是找的網上的一段代碼,直接黏貼複製放【util】包下就能夠了:

/**
 * 用於從Request請求中獲取到客戶端的獲取操做系統,瀏覽器及瀏覽器版本信息
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 8:40
 */
public class BrowserUtil {
    /**
     * 獲取操做系統,瀏覽器及瀏覽器版本信息
     *
     * @param request
     * @return
     */
    public static String getOsAndBrowserInfo(HttpServletRequest request) {
        String browserDetails = request.getHeader("User-Agent");
        String userAgent = browserDetails;
        String user = userAgent.toLowerCase();

        String os = "";
        String browser = "";

        //=================OS Info=======================
        if (userAgent.toLowerCase().indexOf("windows") >= 0) {
            os = "Windows";
        } else if (userAgent.toLowerCase().indexOf("mac") >= 0) {
            os = "Mac";
        } else if (userAgent.toLowerCase().indexOf("x11") >= 0) {
            os = "Unix";
        } else if (userAgent.toLowerCase().indexOf("android") >= 0) {
            os = "Android";
        } else if (userAgent.toLowerCase().indexOf("iphone") >= 0) {
            os = "IPhone";
        } else {
            os = "UnKnown, More-Info: " + userAgent;
        }
        //===============Browser===========================
        if (user.contains("edge")) {
            browser = (userAgent.substring(userAgent.indexOf("Edge")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("msie")) {
            String substring = userAgent.substring(userAgent.indexOf("MSIE")).split(";")[0];
            browser = substring.split(" ")[0].replace("MSIE", "IE") + "-" + substring.split(" ")[1];
        } else if (user.contains("safari") && user.contains("version")) {
            browser = (userAgent.substring(userAgent.indexOf("Safari")).split(" ")[0]).split("/")[0]
                    + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
        } else if (user.contains("opr") || user.contains("opera")) {
            if (user.contains("opera")) {
                browser = (userAgent.substring(userAgent.indexOf("Opera")).split(" ")[0]).split("/")[0]
                        + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
            } else if (user.contains("opr")) {
                browser = ((userAgent.substring(userAgent.indexOf("OPR")).split(" ")[0]).replace("/", "-"))
                        .replace("OPR", "Opera");
            }

        } else if (user.contains("chrome")) {
            browser = (userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0]).replace("/", "-");
        } else if ((user.indexOf("mozilla/7.0") > -1) || (user.indexOf("netscape6") != -1) ||
                (user.indexOf("mozilla/4.7") != -1) || (user.indexOf("mozilla/4.78") != -1) ||
                (user.indexOf("mozilla/4.08") != -1) || (user.indexOf("mozilla/3") != -1)) {
            browser = "Netscape-?";

        } else if (user.contains("firefox")) {
            browser = (userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("rv")) {
            String IEVersion = (userAgent.substring(userAgent.indexOf("rv")).split(" ")[0]).replace("rv:", "-");
            browser = "IE" + IEVersion.substring(0, IEVersion.length() - 1);
        } else {
            browser = "UnKnown, More-Info: " + userAgent;
        }

        return os + "-" + browser;
    }
}

第二步:設置攔截地址

仍是在剛纔的配置類中,新增這麼一條:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用於添加攔截規則
    // excludePathPatterns 用戶排除攔截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin","/admin/**");
    super.addInterceptors(registry);
}

設置默認錯誤頁面

在SpringBoot中,默認的錯誤頁面比較醜(以下),因此咱們能夠本身改得稍微好看一點兒,具體的教程在這裏:http://tengj.top/2018/05/16/springboot13/ ,我就搞前臺的時候再去弄了...


Service 層開發

這是糾結最久應該怎麼寫的,一開始我還準備老老實實地利用MyBatis逆向工程生成的一堆東西去給每個實體建立一個Service的,這樣其實就只是對Dao層進行了一層沒必要要的封裝而已,而後經過分析其實主要的業務也就分紅幾個:文章/評論/分類/日誌瀏覽量這四個部分而已,因此建立這四個Service就行了;

比較神奇的事情是在網上找到一種通用Mapper的最佳實踐方法,整我的都驚了,「wtf?還能夠這樣寫哦?」,資料以下:http://tengj.top/2017/12/20/springboot11/

emmmm..咱們經過MyBatis的逆向工程,已經很大程度上簡化了咱們的開發,由於在Dao層咱們已經免去了本身寫SQL語句,本身寫實體,本身寫XML映射文件的麻煩,但在Service層咱們仍然無可避免的要寫一些相似功能的代碼,有沒有什麼方法能把這些比較通用的方法給提取出來呢? 答案就在上面的連接中,oh,簡直太酷了...我決定在這裏介紹一下...

通用接口開發

在Spring4中,因爲支持了泛型註解,再結合通用Mapper,咱們的想法獲得了一個最佳的實踐方法,下面咱們來說解一下:

第一步:建立通用接口

咱們把一些常見的,通用的方法統一使用泛型封裝在一個通用接口之中:

/**
 * 通用接口
 *
 * @author: wmyskxz
 * @create: 2018年6月15日10:27:04
 */
public interface IService<T> {

    T selectByKey(Object key);

    int save(T entity);

    int delete(Object key);

    int updateAll(T entity);

    int updateNotNull(T entity);

    List<T> selectByExample(Object example);

}

第二步:實現通用接口類

/**
 * 通用Service
 *
 * @param <T>
 */
public abstract class BaseService<T> implements IService<T> {

    @Autowired
    protected Mapper<T> mapper;

    public Mapper<T> getMapper() {
        return mapper;
    }

    /**
     * 說明:根據主鍵字段進行查詢,方法參數必須包含完整的主鍵屬性,查詢條件使用等號
     *
     * @param key
     * @return
     */
    @Override
    public T selectByKey(Object key) {
        return mapper.selectByPrimaryKey(key);
    }

    /**
     * 說明:保存一個實體,null的屬性也會保存,不會使用數據庫默認值
     *
     * @param entity
     * @return
     */
    @Override
    public int save(T entity) {
        return mapper.insert(entity);
    }

    /**
     * 說明:根據主鍵字段進行刪除,方法參數必須包含完整的主鍵屬性
     *
     * @param key
     * @return
     */
    @Override
    public int delete(Object key) {
        return mapper.deleteByPrimaryKey(key);
    }

    /**
     * 說明:根據主鍵更新實體所有字段,null值會被更新
     *
     * @param entity
     * @return
     */
    @Override
    public int updateAll(T entity) {
        return mapper.updateByPrimaryKey(entity);
    }

    /**
     * 根據主鍵更新屬性不爲null的值
     *
     * @param entity
     * @return
     */
    @Override
    public int updateNotNull(T entity) {
        return mapper.updateByPrimaryKeySelective(entity);
    }

    /**
     * 說明:根據Example條件進行查詢
     * 重點:這個查詢支持經過Example類指定查詢列,經過selectProperties方法指定查詢列
     *
     * @param example
     * @return
     */
    @Override
    public List<T> selectByExample(Object example) {
        return mapper.selectByExample(example);
    }
}

至此呢,咱們的通用接口就開發完成了

第三步:使用通用接口

編寫好咱們的通用接口以後,使用就變得很方便了,只須要繼承相應的通用接口或者通用接口實現類,而後進行簡單的封裝就好了,下面以SortInfo爲例:

public interface SortInfoService extends IService<SortInfo> {
}
========================分割線========================
/**
 * 分類信息Service
 *
 * @author:wmyskxz
 * @create:2018-06-15-上午 11:14
 */
@Service
public class SortInfoServiceImpl extends BaseService<SortInfo> implements SortInfoService {
}

對應到SortInfo的RESTful API設計,這樣簡單的繼承就可以很好的支持,可是咱們仍是使用最原始的方式來建立吧...

Service接口申明

查了一些資料,問了一下實習公司的前輩老師,而且根據咱們以前設計好的RESTful APIs,咱們頗有必要搞一個dto層用於先後端之間的數據交互,這一層主要是對數據庫的數據進行一個封裝整合,也方便先後端的數據交互,因此咱們首先就須要分析在dto層中應該存在哪些數據:

DTO層開發

對應咱們的業務邏輯和RESTful APIs,我大概弄了下面幾個Dto:

① ArticleDto:

該Dto封裝了文章的詳細信息,對應RESTful API中的/api/article/{id}——經過文章ID獲取文章信息

/**
 * 文章信息類
 * 說明:關聯了tbl_article_info/tbl_article_content/tbl_article_category/tbl_category_info/
 * tbl_article_picture五張表的基礎字段
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:13
 */
public class ArticleDto {

    // tbl_article_info基礎字段
    private Long id;
    private String title;
    private String summary;
    private Boolean isTop;
    private Integer traffic;

    // tbl_article_content基礎字段
    private Long articleContentId;
    private String content;

    // tbl_category_info基礎字段
    private Long categoryId;
    private String categoryName;
    private Byte categoryNumber;

    // tbl_article_category基礎字段
    private Long articleCategoryId;

    // tbl_article_picture基礎字段
    private Long articlePictureId;
    private String pictureUrl;

    /* getter and setter */
}

②ArticleCommentDto:

該Dto封裝的事文章的評論信息,對應/api/comment/article/{id}——經過文章ID獲取某一篇文章的所有評論信息

/**
 * 文章評論信息
 * 說明:關聯了tbl_comment和tbl_article_comment兩張表的信息
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:09
 */
public class ArticleCommentDto {
    // tbl_comment基礎字段
    private Long id;                // 評論id
    private String content;         // 評論內容
    private String name;            // 用戶自定義的顯示名稱
    private String email;
    private String ip;

    // tbl_article_comment基礎字段
    private Long articleCommentId;  // tbl_article_comment主鍵
    private Long articleId;         // 文章ID

    /* getter and setter */
}

③ArticleCategoryDto:

該Dto是封裝了文章的一些分類信息,對應/admin/category/{id}——獲取某一篇文章的分類信息

/**
 * 文章分類傳輸對象
 * 說明:關聯了tbl_article_category和tbl_category_info兩張表的數據
 *
 * @author:wmyskxz
 * @create:2018-06-20-上午 8:45
 */
public class ArticleCategoryDto {

    //  tbl_article_category表基礎字段
    private Long id;            // tbl_article_category表主鍵
    private Long categoryId;    // 分類信息ID
    private Long articleId;     // 文章ID

    // tbl_category_info表基礎字段
    private String name;        // 分類信息顯示名稱
    private Byte number;        // 該分類下對應的文章數量

    /* getter and setter */
}

④ArticleWithPictureDto:

該Dto封裝了文章用於顯示的基本信息,對應全部的獲取文章集合的RESful APIs

/**
 * 帶題圖信息的文章基礎信息分裝類
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:53
 */
public class ArticleWithPictureDto {
    // tbl_article_info基礎字段
    private Long id;
    private String title;
    private String summary;
    private Boolean isTop;
    private Integer traffic;

    // tbl_article_picture基礎字段
    private Long articlePictureId;
    private String pictureUrl;

    /* getter and setter */
}

Service接口開發

Service層其實就是對咱們業務的一個封裝,因此有了RESTful APIs文檔,咱們能夠很輕易的寫出對應的業務模塊:

文章Service

/**
 * 文章Service
 * 說明:ArticleInfo裏面封裝了picture/content/category等信息
 */
public interface ArticleService {

    void addArticle(ArticleDto articleDto);

    void deleteArticleById(Long id);

    void updateArticle(ArticleDto articleDto);

    void updateArticleCategory(Long articleId, Long categoryId);

    ArticleDto getOneById(Long id);

    ArticlePicture getPictureByArticleId(Long id);

    List<ArticleWithPictureDto> listAll();

    List<ArticleWithPictureDto> listByCategoryId(Long id);

    List<ArticleWithPictureDto> listLastest();
}

分類Service

/**
 * 分類Service
 */
public interface CategoryService {
    void addCategory(CategoryInfo categoryInfo);

    void deleteCategoryById(Long id);

    void updateCategory(CategoryInfo categoryInfo);

    void updateArticleCategory(ArticleCategory articleCategory);

    CategoryInfo getOneById(Long id);

    List<CategoryInfo> listAllCategory();

    ArticleCategoryDto getCategoryByArticleId(Long id);
}

留言Service

/**
 * 留言的Service
 */
public interface CommentService {
    void addComment(Comment comment);

    void addArticleComment(ArticleCommentDto articleCommentDto);

    void deleteCommentById(Long id);

    void deleteArticleCommentById(Long id);

    List<Comment> listAllComment();

    List<ArticleCommentDto> listAllArticleCommentById(Long id);
}

系統Service

/**
 * 日誌/訪問統計等系統相關Service
 */
public interface SysService {
    void addLog(SysLog sysLog);

    void addView(SysView sysView);

    int getLogCount();

    int getViewCount();

    List<SysLog> listAllLog();

    List<SysView> listAllView();
}

Controller 層開發

Controller層簡單理解的話,就是用來獲取數據的,因此只要Service層開發好了Controller層就很容易,就很少說了,只是咱們能夠把一些公用的東西放到一個BaseController中,好比引入Service:

/**
 * 基礎控制器
 *
 * @author:wmyskxz
 * @create:2018-06-19-上午 11:25
 */
public class BaseController {
    @Autowired
    ArticleService articleService;
    @Autowired
    CommentService commentService;
    @Autowired
    CategoryService categoryService;
}

而後先後臺的控制器只須要繼承該類就好了,這樣的方式很是值得借鑑的,只是由於這個系統比較簡單,因此這個BaseController,我看過一些源碼,能夠在裏面弄一個通用的用於返回數據的方法,好比分頁數據/錯誤信息之類的;


記錄坑

1)MyBatis中Text類型的坑

按照《阿里手冊》(簡稱)上所規範的那樣,我把文章的content單獨弄成了一張表而且將這個「可能很長」的字段的類型設置成了text類型,可是MyBatis逆向工程自動生成的時候,卻把這個text類型的字段單獨給列了出去,即在生成的xml中多出了一個<resultMap>,標識id爲ResultMapWithBLOBs,MyBatis這樣作可能的緣由仍是怕這個字段太長影響前面的字段查詢吧,可是操做這樣的LONGVARCHAR類型的字段MyBatis好像並無集成很好,因此想要很好的操做仍是須要給它弄成VARCHAR類型才行;

在generatorConfig.xml中配置生成字段的時候加上這樣一句話就行了:

<table domainObjectName="ArticleContent" tableName="tbl_article_content">  
    <columnOverride column="content" javaType="java.lang.String" jdbcType="VARCHAR" />  
</table>

2)攔截器中Service注入爲null的坑

在編寫前臺攔截器的時候,我使用@Autowired註解自動注入了SysService系統服務Service,可是卻報nullpointer的錯,發現是沒有自動注入上,SysService爲空..這是爲何呢?排除掉註解沒有識別或者沒有給Service添加上註解的可能性以後,我發現好像是攔截器攔截的時候Service並無建立成功形成的,參考這篇文章:https://blog.csdn.net/slgxmh/article/details/51860278,成功解決問題:

@Bean
public HandlerInterceptor getForeInterceptor() {
    return new ForeInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用於添加攔截規則
    // excludePathPatterns 用戶排除攔截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
   registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin", "/admin/**");
    super.addInterceptors(registry);
}

其實就是添加上@Bean註解讓ForeInterceptor提早加載;

3)數據庫sys_log表中operate_by字段的坑

當時設計表的時候,就只是單純的想要保存一下用戶使用的瀏覽器是什麼,其實當時並不知道應該怎麼獲取獲取到的東西又是什麼,只是以爲保存瀏覽器20個字段夠了,但後來發現這是很蠢萌的...因此不得不調整數據庫的字段長度,好在只須要單方面調整數據庫的字段長度就行了:

4)保存文章的方式的坑

由於我想要在數據庫中保存的是md源碼,而返回前臺前端但願的是直接拿到html代碼,這樣就能很方便的輸出了,因此這要怎麼作呢?找到一篇參考文章:https://my.oschina.net/u/566591/blog/1535380

咱們不要搞那麼複雜的封裝,只要簡單弄一個工具類就能夠了,在【util】包下新建一個【Markdown2HtmlUtil】:

/**
 * Markdown轉Html工具類
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 10:09
 */
public class Markdown2HtmlUtil {
    /**
     * 將markdown源碼轉換成html返回
     *
     * @param markdown md源碼
     * @return html代碼
     */
    public static String markdown2html(String markdown) {
        MutableDataSet options = new MutableDataSet();
        options.setFrom(ParserEmulationProfile.MARKDOWN);
        options.set(Parser.EXTENSIONS, Arrays.asList(new Extension[]{TablesExtension.create()}));
        Parser parser = Parser.builder(options).build();
        HtmlRenderer renderer = HtmlRenderer.builder(options).build();

        Node document = parser.parse(markdown);
        return renderer.render(document);
    }
}

使用也很簡單,只須要在獲取一篇文章的時候把ArticleDto裏面的md源碼轉成html代碼再返回給前臺就行了:

/**
 * 經過文章的ID獲取對應的文章信息
 *
 * @param id
 * @return 本身封裝好的文章信息類
 */
@ApiOperation("經過文章ID獲取文章信息")
@GetMapping("article/{id}")
public ArticleDto getArticleById(@PathVariable Long id) {
    ArticleDto articleDto = articleService.getOneById(id);
    articleDto.setContent(Markdown2HtmlUtil.markdown2html(articleDto.getContent()));
    return articleDto;
}

樣式之類的交給前臺就行了,搞定...


簡單總結

關於統計啊日誌類的Controller尚未開發,RESful API也沒有設計,這裏就先發布文章了,由於好像時間有點緊,後臺的頁面暫時可能開發不完,準備直接開始前臺頁面顯示的開發(主要是本身對前端不熟悉還要學習..),這裏對後臺進行一個簡單的總結:

其實發現當數據庫設計好了,RESful APIs設計好了以後,後臺的任務變得很是明確,開發起來也就思路很清晰了,只是本身仍是缺乏一些必要的經驗,如對一些通用方法的抽象/層與層之間數據交互的典型設計之類的東西,特別是一些安全方面的東西,網上的資料也比較少一些,也是本身須要學習的地方;

歡迎轉載,轉載請註明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz 歡迎關注公衆微信號:wmyskxz_javaweb 分享本身的Java Web學習之路以及各類Java學習資料

相關文章
相關標籤/搜索