1. 選題背景
1.1 選題概述
很早之前就想要寫一個自己的博客了,趁着現在學校安排的web課設選題,決定把它給做出來,也順便復習一下曾經學過的一些web技術和框架。
只有動手做完之后,才能發現不足之處,比如一些細節的處理,大體的表設計,業務邏輯接口的編寫,以及一些bug的存在,還可以讓自己更熟練的開發各種功能的網頁。
1.2 技術選型
后端技術:Springboot + Spring + Mybatis + Druid + Swagger + 熱部署 + Mysql
前端技術:html+css+js+Jquery+bootstrap+vue.js
2. 總體系統功能模塊
2.1 需求分析
前端需求分析:
①簡潔/美觀——個人很喜歡像Mac那樣的簡潔風,越簡單越好,當然也得好看;(首頁輪播圖+分類左右排版+導航欄+博文詳情頁)
②最好是單頁面——單頁面的目的一方面是為了簡潔,另一方面也是為了實現起來比較簡單;(單頁面就不用vue.js做SPA了,還是通過a標簽原地跳轉的方式模擬單頁面)
③自適應——至少能適配常見的手機分辨率吧,我可不希望自己的博客存在顯示差異性的問題;(Bootstrap的柵格系統+CSS媒體查詢+配合JS實現)
可能出現的頁面如圖:

圖1
PS:留言頁和關於頁,簡歷頁以后再實現

圖2
PS:評論功能暫未實現,只實現了博文分類(CURD)和文章管理(常見的CURD)的功能

圖3
PS:數據統計模塊暫未實現
2.2 表結構設計
個人博客系統數據結構設計:

表1
PS:此圖用Navicat 的表逆向模型 的功能實現
2.3 表結構分析
1)分類信息表(tbl_category_content):
CREATE TABLE `tbl_category_info` (
`id` bigint(40) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL COMMENT '分類名稱',
`number` tinyint(10) NOT NULL DEFAULT '0' COMMENT '該分類下的文章數量',
`create_by` datetime NOT NULL COMMENT '分類創建時間',
`modified_by` datetime NOT NULL COMMENT '分類修改時間',
`is_effective` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,默認為1有效,為0無效',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
表2
2)文章內容表(tbl_article_content):
CREATE TABLE `tbl_article_content` (
`id` bigint(40) NOT NULL AUTO_INCREMENT,
`content` text NOT NULL,
`article_id` bigint(40) NOT NULL COMMENT '對應文章ID',
`create_by` datetime NOT NULL COMMENT '創建時間',
`modifield_by` datetime NOT NULL COMMENT '更新時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
表2
PS: 文章內容單獨分一個表是因為要把MD格式的文章直接從后台添加到數據庫中,屬於大文本類型,不放在文章基礎信息表中,是為了查詢效率,不需要索引大文本域
3)文章信息表(tbl_article_info):
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 COMMENT '創建時間',
`modified_by` datetime NOT NULL COMMENT '修改日期',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
表3
下面為關聯表
PS:用關聯表,是為了不讓后端做多表連接查詢,影響查詢效率,所以也不需要建立外鍵,
讓后端在Service層手動完成外鍵的功能,大大減少了數據庫的壓力。
4)文章分類表(tbl_article_category):
CREATE TABLE `tbl_article_category` (
`id` bigint(40) NOT NULL AUTO_INCREMENT,
`sort_id` bigint(40) NOT NULL COMMENT '分類id',
`article_id` bigint(40) NOT NULL COMMENT '文章id',
`create_by` datetime NOT NULL COMMENT '創建時間',
`modified_by` datetime NOT NULL COMMENT '更新時間',
`is_effective` tinyint(1) DEFAULT '1' COMMENT '表示當前數據是否有效,默認為1有效,0則無效',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
表4
5)文章題圖表(tbl_article_picture):
CREATE TABLE `tbl_article_picture` (
`id` bigint(40) NOT NULL AUTO_INCREMENT,
`article_id` bigint(40) NOT NULL COMMENT '對應文章id',
`picture_url` varchar(100) NOT NULL DEFAULT '' COMMENT '圖片url',
`create_by` datetime NOT NULL COMMENT '創建時間',
`modified_by` datetime NOT NULL COMMENT '更新時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='這張表用來保存題圖url,每一篇文章都應該有題圖';
表5
PS:題圖用於前端首頁的輪播圖和文章詳情頁文章的配圖
3. 原型參考
3.1 前端原型參考
首頁:

圖4
文章分類頁:

圖5
文章詳情頁:

圖6
3.2 后端原型參考

圖7
4.項目搭建
4.1 Springboot項目配置


圖8
PS:上圖為從Springboot官網Springboot initializer 后搭建的Springboot項目,在上面進行
二次開發后,最后的開發的目錄結構如上
Maven工程的依賴為:Pom文件如下

圖9
對項目目錄結構進行簡要說明:
- controller:控制器 (MVC的C模塊,用於處理url映射請求以及ResfulAPI的設計)
- dao:實際上這個包可以改名叫mapper,因為里面放的應該是MyBatis逆向工程自動生成之后的mapper類。(就是數據訪問對象層,訪問數據庫的,增刪改查的方法都在這里)
- entity:實體類,(MVC中M模塊,Model,對應表的JavaBean)還會有一些MyBatis生成的example
- generator:MyBatis逆向工程生成類
- interceptor:SpringBoot 攔截器 (攔截后台管理系統的請求,判斷有無管理員登陸的權限)
- service:Service層,里面還有一層impl目錄 (業務邏輯接口的開發都在這里)
- util:一些工具類可以放在里面 (Markdown格式轉html的工具類也在這里)
- mapper:用於存放MyBatis逆向工程生成的.xml映射文件
- static:這個目錄存放一些靜態文件,簡單了解了一下Vue的前后端分離,前台文件以后也需要放在這個目錄下面(放網頁和JS,CSS,image的地方)
4.2 Mybatis框架集成配置
1. Springboot繼承Mybatis是通過依賴starter來集成Mybatis框架的
Pom依賴如下:

圖10
2. Mybatis逆向工程:

圖11
PS:逆向工程用於自動根據配置的數據庫來生成Entity類,和mapper映射文件和mapper映射接口(用來操作數據庫的),相當於自動生成了一大堆的sql語句(增刪改查),上一層直接調用DAO層的接口即可訪問數據庫 (松耦合)
4.3 Restful設計與Swagger2配置
1.概要:RestfulAPI是一種HTTP請求的規范,可以用到put請求表示更新數據,
Delete請求表示刪除數據,post請求表示添加數據,get請求表示查詢數據,合理的運用
HTTP方法來完成請求,避免了以前WEB開發只用get 和post請求的這種不規范設計
格式為下圖:

圖12
2. Swaager文檔用於圖形化RestfulAPI風格的接口,效果如下圖:

圖13
4.4 數據庫連接池配置和日志配置
1. 采用了Druid數據庫連接池(當今最實用,效率也很高的阿里巴巴的連接池)

圖14
2. 日志配置: Springboot天生集成了logback日志,所以不需要再重新導入新的日志框架,
直接復制日志配置文件即可,但注意名字要按格式來,才能被加載,如圖:

圖15
4.5 攔截器配置
登陸攔截器代碼如下:(還用Cookie實現了30分鍾有效期的自動登陸)
public class BackInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//通過session判斷是否已經登陸
String username = (String)request.getSession().getAttribute("username");
String name = "zhanp";
//傳輸的加密先放一邊,后面再看下
//先判斷Session不為空,說明已經登陸了
if(StringUtils.isEmpty(username)){
Cookie[] cookies = request.getCookies();
//判斷cookie中有沒有自動登陸的憑證
if(cookies!=null){
for(Cookie cookie:cookies){
if(!StringUtils.isEmpty(cookie)&&cookie.getName().equals(name)){
return true;
}
}
}else{
return false;
}
}
return true;
}
}
5. 后端開發過程
5.1 Entity層開發

圖16

圖17
這些實體類對應的是Mysql中建立的表的名字,屬性名字為表的字段名
5.2 Service層開發

圖18
1. 比如文章的業務接口開發有
1.1 添加文章->要填充文章內容表,文章-分類表,文章-題圖表,文章信息表,還要修改相應分類下的文章數目
public void addArticle(ArticleDto articleDto) {
//1.填充文章信息表----title/summary/isTop
// 前端不可能給你Id的,這是后端自動生產的,要在后端獲取Id
// Long id = articleDto.getId();
String title = articleDto.getTitle();
String summary = articleDto.getSummary();
Boolean isTop = articleDto.getIsTop();
ArticleInfo articleInfo = new ArticleInfo();
articleInfo.setTitle(title);
articleInfo.setSummary(summary);
articleInfo.setIsTop(isTop);
//1.1 寫入文章信息表中
//1.2 並查詢新增的文章Id。。。因為返回主鍵也需要select和插入處於同一事務,所以不會返回正確的插入后的主鍵
articleInfoMapper.insertSelective(articleInfo) ;
//從參數里返回主鍵
Long id = articleInfo.getId();
//2. 填充文章-內容表----文章Id/content
ArticleContent articleContent = new ArticleContent();
articleContent.setArticleId(id);
articleContent.setContent(articleDto.getContent());
//2.1 寫入文章-內容表
articleContentMapper.insertSelective(articleContent);
//3. 填充文章 - 分類表---文章Id/分類Id
ArticleCategory articleCategory = new ArticleCategory();
articleCategory.setArticleId(id);
articleCategory.setSortId(articleDto.getCategoryId());
//3.1 寫入文章 - 分類表
articleCategoryMapper.insertSelective(articleCategory);
//3.2 分類下的文章信息 + 1
Long sortId = articleCategory.getSortId();
//查詢你源分類信息條目
CategoryInfo categoryInfo = categoryInfoMapper.selectByPrimaryKey(sortId);
//文章+1
categoryInfo.setNumber((byte) (categoryInfo.getNumber()+1));
categoryInfoMapper.updateByPrimaryKeySelective(categoryInfo);
//4. 填充文章-題圖表 ---文章Id/圖片url
ArticlePicture articlePicture = new ArticlePicture();
articlePicture.setArticleId(id);
articlePicture.setPictureUrl(articleDto.getPictureUrl());
//4.1寫入 文章-題圖表
articlePictureMapper.insertSelective(articlePicture);
}
1.2 更新文章:
* 根據封裝的ArticleDto參數 選擇性的更新文章
* warning: ArticleDto參數后台按實際情況應該只有文章基礎信息的Id,和圖片url,內容content,分類Id這種,
* 而不會有從表的主鍵Id,所以除了文章信息表外,其他從表需要根據文章Id關聯查詢出來
* 比如更新文章基礎信息(title,summary,isTop)
* 更新文章-分類表的信息
* 更新文章-題圖表的信息
*
* 還有更新文章時分類信息改了的話,要調用分類文章-的api updateArticleCategory()去重新統計分類下的數目,這個寫漏了
@Override
public void updateArticle(ArticleDto articleDto) {
Long id = articleDto.getId();
//1.文章基礎信息表
//1.1 填充ArticleInfo參數
ArticleInfo articleInfo = new ArticleInfo();
articleInfo.setId(id);
articleInfo.setSummary(articleDto.getSummary());
articleInfo.setIsTop(articleDto.getIsTop());
articleInfo.setTitle(articleDto.getTitle());
articleInfo.setTraffic(articleDto.getTraffic());
articleInfoMapper.updateByPrimaryKeySelective(articleInfo);
//2. 文章-分類表
//根據文章Id----找出對應的文章分類表 的條目
ArticleCategoryExample articleCategoryExample = new ArticleCategoryExample();
ArticleCategoryExample.Criteria articleCategoryExampleCriteria = articleCategoryExample.createCriteria();
articleCategoryExampleCriteria.andArticleIdEqualTo(id);
List<ArticleCategory> articleCategoryList = articleCategoryMapper.selectByExample(articleCategoryExample);
ArticleCategory category = articleCategoryList.get(0);
//2.1 先檢查源分類Id與更新過來的分類Id是否相等
// 如果分類被修改過了,那么分類下的文章數目也要修改
//前者是源Id,后者是更新過來的Id
Long sourceSortId = category.getSortId();
Long categoryId = articleDto.getCategoryId();
if(!sourceSortId.equals(categoryId)){
//2.3 更新分類下的文章信息
updateArticleCategory(id,categoryId);
}
//3.文章-題圖表
ArticlePictureExample articlePictureExample = new ArticlePictureExample();
articlePictureExample.or().andArticleIdEqualTo(id);
List<ArticlePicture> pictureList = articlePictureMapper.selectByExample(articlePictureExample);
ArticlePicture articlePicture = pictureList.get(0);
articlePicture.setPictureUrl(articleDto.getPictureUrl());
articlePictureMapper.updateByPrimaryKeySelective(articlePicture);
//4.文章-內容表
ArticleContentExample articleContentExample = new ArticleContentExample();
articleContentExample.or().andArticleIdEqualTo(id);
List<ArticleContent> contentList = articleContentMapper.selectByExample(articleContentExample);
ArticleContent articleContent = contentList.get(0);
articleContent.setContent(articleDto.getContent());
articleContentMapper.updateByPrimaryKeyWithBLOBs(articleContent);
}
1.3 獲取一篇文章(根據文章Id)
@Override
public ArticleDto getOneById(Long id) {
ArticleDto articleDto = new ArticleDto();
//1. 文章信息表內的信息 填充 到 Dto
ArticleInfo articleInfo = articleInfoMapper.selectByPrimaryKey(id);
//1.1 增加瀏覽量 + 1
ArticleInfo info = new ArticleInfo();
info.setId(id);
info.setTraffic(articleInfo.getTraffic()+1);
articleInfoMapper.updateByPrimaryKeySelective(info);
articleDto.setId(id);
articleDto.setTitle(articleInfo.getTitle());
articleDto.setSummary(articleInfo.getSummary());
articleDto.setIsTop(articleInfo.getIsTop());
//沒用到緩存,所以訪問量統計還是在SQL操作這里增加把(一個博客,做啥緩存啊)
articleDto.setCreateBy(articleInfo.getCreateBy());
articleDto.setTraffic(articleInfo.getTraffic()+1);
//2. 文章內容表內的信息 填充 到 Dto
ArticleContentExample articleContentExample = new ArticleContentExample();
articleContentExample.or().andArticleIdEqualTo(id);
List<ArticleContent> contentList = articleContentMapper.selectByExampleWithBLOBs(articleContentExample);
ArticleContent articleContent = contentList.get(0);
articleDto.setContent(articleContent.getContent());
//填充關聯表的主鍵,其他業務可能通過調用getOneById 拿到Dto里的這個主鍵
articleDto.setArticleContentId(articleContent.getId());
//3.文章-分類表內的信息 填充 到 Dto
ArticleCategoryExample articleCategoryExample = new ArticleCategoryExample();
articleCategoryExample.or().andArticleIdEqualTo(id);
List<ArticleCategory> articleCategories = articleCategoryMapper.selectByExample(articleCategoryExample);
ArticleCategory articleCategory = articleCategories.get(0);
//3.1設置文章所屬的分類Id+ 從表主鍵 --從表
Long sortId = articleCategory.getSortId();
articleDto.setCategoryId(sortId);
articleDto.setArticleCategoryId(articleCategory.getId());
//3.2找分類主表 --設置分類信息
CategoryInfo categoryInfo = categoryInfoMapper.selectByPrimaryKey(sortId);
articleDto.setCategoryName(categoryInfo.getName());
articleDto.setCategoryNumber(categoryInfo.getNumber());
//4.文章-題圖表
ArticlePictureExample articlePictureExample = new ArticlePictureExample();
articlePictureExample.or().andArticleIdEqualTo(id);
List<ArticlePicture> articlePictures = articlePictureMapper.selectByExample(articlePictureExample);
ArticlePicture picture = articlePictures.get(0);
//4.1設置圖片Dto
articleDto.setArticlePictureId(picture.getId());
articleDto.setPictureUrl(picture.getPictureUrl());
return articleDto;
}
1.4 找出分類下所有的文章信息
@Override
public List<ArticleWithPictureDto> listByCategoryId(Long id) {
//1. 先找出分類下所有的文章
ArticleCategoryExample articleCategoryExample = new ArticleCategoryExample();
articleCategoryExample.or().andSortIdEqualTo(id);
List<ArticleCategory> articleCategories = articleCategoryMapper.selectByExample(articleCategoryExample);
ArrayList<ArticleWithPictureDto> list = new ArrayList<>();
//1.1遍歷
for(ArticleCategory articleCategory:articleCategories){
ArticleWithPictureDto articleWithPictureDto = new ArticleWithPictureDto();
//1.1.1 取出文章
Long articleId = articleCategory.getArticleId();
ArticleInfo articleInfo = articleInfoMapper.selectByPrimaryKey(articleId);
//1.1.2 取出文章對應的圖片url
ArticlePictureExample articlePictureExample = new ArticlePictureExample();
articlePictureExample.or().andArticleIdEqualTo(articleId);
List<ArticlePicture> articlePictures = articlePictureMapper.selectByExample(articlePictureExample);
ArticlePicture picture = articlePictures.get(0);
articleWithPictureDto.setId(articleId);
articleWithPictureDto.setArticlePictureId(picture.getId());
articleWithPictureDto.setTitle(articleInfo.getTitle());
articleWithPictureDto.setSummary(articleInfo.getSummary());
articleWithPictureDto.setIsTop(articleInfo.getIsTop());
articleWithPictureDto.setTraffic(articleInfo.getTraffic());
articleWithPictureDto.setPictureUrl(picture.getPictureUrl());
list.add(articleWithPictureDto);
}
return list;
}
PS:還有一系列的接口開發在源碼中查看吧
5.3 DTO層開發

圖19
用於封裝了多個實體類的屬性,用於前后端交互的整體屬性封裝,便捷實用的進行
JSON數據交互
5.4 Controller層開發

圖20
BaseController為后台控制器
ForeController為前台控制器
比如更新文章的Controller

圖21

圖22

圖23
6. 前端開發過程
6.1 登陸頁開發
Login.html


圖24

圖25
效果如下:

圖26
還用了輪播圖的形式

圖27
url:為toLogin,代碼實現為:

圖28
6.2 分類管理頁開發

圖29
Category.html

圖30
圖31
圖32
效果如下:
圖33
6.3博文管理頁開發

圖34

圖35
效果如下:

圖36

圖37

PS:還用了動態的placeholder來保存更新前的數據,在此上面做修改。這個是模態框
圖38
PS:這些分類都是動態從數據庫里拉過來的,不是靜態的!!!
6.4 博客首頁開發

圖39

圖40
導航欄 + 最新幾篇文章的輪播圖(點擊可進入文章詳情頁)
6.5 博客文章詳情頁開發
代碼如下:

圖41
效果如下:

圖42

圖43
PS:這些都是動態的,非靜態頁面,靜態就沒有意義了
6.6 博文分類頁面

圖44
效果如下:

圖45

圖46

圖47
PS:還設置了動態的分類選中效果,根據不同的分類顯示不同的文章信息,點擊文章信息,即可進入文章詳情頁
7. 項目心得總結
1. 以后要多加練習,多做項目來熟悉一般web項目的整個開發流程,比如搭建項目的環境,相應框架的配置。
2. 還要多總結開發過程中遇到的bug和一些細節的處理,比如這個效果怎么實現,這個功能用什么方法實現,要寫個筆記好好記錄一下,方便以后的開發,
不需要再次查詢百度或谷歌。
3. 還要重視數據庫,不要以為只會寫幾條增刪改查的sql語句即可,關鍵是
對數據庫的設計,對表的編排,關聯表的運用,如何設計表結構讓程序跑的更快,開發更方便。還要重視數據庫的索引技術,分表分庫,以后都可以深造
4. 不要停留在只知道這個技術,而不去動手實踐,這些知識不實踐就會忘。
比如Mybatis配置文件和框架整合,或Spring的配置,或Springboot的錯誤處理頁面的定制,或者Thymeleaf模板引擎的熟練使用(雖然前后端分離以及不用這種類似JSP的模板引擎了),或者是事務的添加,又或者前后端密碼校驗的加密處理,以及前端CSS的布局,樣式的熟練掌握,bootstrap常用的樣式的實現,vue.js的細節和bug等等。
5.但是又不能停留在只會用這些表面的框架和技術,而不懂其原理,基礎和原理是很重要的,對於后期的debug排查錯誤,對原理熟悉的,可以很快的找尋出是哪方面導致的問題。而且Spring框架的IOC和AOP概念貫穿了整個Spring全家桶的產品,所以一定要深刻理解和練習,還有對於Java基礎的提高,比如反射技術(對應於Spring中的AOP實現,事務的實現,自動配置類的加載,動態代理等等)都用到反射技術。
