个人博客开发笔记
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实现,事务的实现,自动配置类的加载,动态代理等等)都用到反射技术。
推荐阅读
-
Android开发笔记之:如何屏蔽Button setClickable与setEnabled
-
Android开发笔记之:复写按钮方法
-
Android开发笔记之:如何安全中止一个自定义线程Thread的方法
-
Android开发笔记之:Log图文详解(Log.v,Log.d,Log.i,Log.w,Log.e)
-
Android开发笔记之:在ImageView上绘制圆环的实现方法
-
Android开发笔记之:返回键的复写onBackPressed()介绍
-
卢松松博客改版了,不再是个人博客了。
-
一个7年的个人博客是如何日赚千元的?
-
HTML5移动开发学习笔记之Canvas基础
-
Layabox 3D游戏开发学习笔记---射线检测,鼠标控制物体运动