第 12 章
? 博客系统项目实战 ?
12.1 项目目的
本博客系统项目目的如下:
? 记载个人学习、工作、生活上一些值得回味的事情,以及一些值得分享或者探讨的技术。
? 用于社会沟通和交友,和他人分享自己的成功。
? 自我学习、自我提高。
12.2 需求分析
提到博客,大部分人都不会陌生,毕竟大名鼎鼎的wordpress可是业界神话。本章需要实现的也是一个博客系统。当然,并没有wordpress那么强大,不过“麻雀虽小、五脏俱全”,一个博客应有的功能还是需要有的。
写作。博客的核心功能就是写作,而且是独自写作,有写作就有文章,有文章就涉及文章的分类、发表、编辑、删除。
评论。既然项目目的中有“用于社会沟通和交友”,那么社会上的读者如何与作者互动呢?所以,评论功能必不可少。有了评论就需要发表评论、管理评论。
友情链接。好文章如何让别人知道呢?单凭自己的力量是不够的,所以合理地与他人交换友情链接是博客的一种推广手段。
12.3 功能设计
通过需求分析的结果,可以总结出博客系统需要以下功能:
? 管理员登录、修改密码、退出登录。
? 文章分类添加、编辑、删除。
? 文章添加、编辑、删除。
? 发表评论、管理评论。
? 添加友情链接、删除友情链接、展示友情链接。
12.4 数据库设计
根据需求分析以及功能设计,设计出如图12-1所示数据库模型。
图12-1
可以看到分类表、文章表、评论表之间存在关系。
12.5 数据库字典
1. 文章分类(blog_category)
文章分类表设计如表12-1所示。
表12-1
字段名称 类型 说明
categoryId int(10) 主键,自增
name varchar(20) 分类名称
isNav tinyint(1) 是否显示在导航栏
total int 文章总数
sort tinyint(4) 排序
2. 文章表(blog_article)
文章表设计如表12-2所示。
表12-2
字段名称 类型 说明
articleId int(11) 主键,自增
Title varchar(40) 文章标题
Description varchar(100) 文章简介
Image varchar(128) 文章封面
Hits int(11) 点击数
createdAt int(11) 文章发布时间(时间戳)
updateAt int(11) 文章更新时间
Status tinyint(1) 状态(发表,不发表)
Sort int 文章排序
Content text 文章正文
categoryId int 分类ID
3. 文章评论表(blog_comment)
文章评论表设计如表12-3所示。
表12-3
字段名称 字段类型 说明
commentId int 主键,自增
nickname varchar(20) 昵称
createdAt int(11) 评论时间
createdIp varchar(15) 评论IP(只考虑IPV4)
content text 评论内容
articleId int 文章ID
4. 管理员表(blog_admin)
管理员表设计如表12-4所示。
表12-4
字段名称 字段类型 说明
adminId int 管理员ID
username varchar(20) 用户名
password char(32) 密码(md5加密后密文)
createdAt int 账号添加时间
loginAt int 最近登录时间
loginIp int 最近登录IP
5. 友情链接表(blog_link)
友情链接表设计如表12-5所示。
表12-5
字段名称 字段类型 说明
linkId int 主键,自增
name varchar(20) 网站名称
link varchar(100) 链接地址
status tinyint(1) 状态
sort int 排序
12.6 模块设计
12.6.1 Admin模块
admin为后台管理模块,需要管理文章、分类、评论、友情链接等功能。所以根据功能应该分开4个Controller进行处理。Controller如下:
? ArticleController,文章控制器。
? CategoryController,分类控制器。
? CommentController,评论控制器。
? LinkController,友情链接控制器。
1. 权限检测
由于admin模块属于受保护的模块,所以以上4个控制器必须登录后才能正常访问,为了不写重复代码,需要新建一个控制器处理登录检测,以上4个控制器继承该基本控制器实现统一权限检测。
在Admin模块新建BaseController.class.php,添加_initialize方法,代码如下:
protected function _initialize()
{
if (session('admin.adminId') === null)
{
$this->error('请登录', U('admin/index/login'));
}
C('LAYOUT_NAME', 'admin');
}
需要进行权限检测的控制器继承BaseController即可。
2. 分页处理
由于该博客系统是一直在线上运行的,所以数据量不可预测,在列表页需要进行分页处理。以下是友情链接主页的分页代码:
public function index()
{
$model = new Model('Link');
$count = $model->count();
$page = new Page($count);
$show = $page->show();
$list = $model->order('linkId DESC')->limit($page->firstRow . ',' . $page->listRows)->select();
$this->assign('list', $list);
$this->assign('page', $show);
$this->display();
}
3. 文章-分类模型
文章是属于分类的,所以读取文章列表的时候需要将分类信息同时查询处理,这里使用ThinkPHP提供的ViewModel,在Common模块新建Model文件夹,在Model文件夹下新建ArticleCategoryViewModel.class.php,代码如下:
array('articleId', 'title', 'description', 'image', 'hits', 'createdAt', 'updateAt', 'status', 'sort', 'content'),
'Category' => array('categoryId', 'name', '_on' => 'Article.categoryId=Category.categoryId')
);
}
ViewModel的知识可以在第5章第9节查看。
4. 文件上传
在设计文章表的时候,有个封面字段,这个字段是用来保存文章封面的,所以需要做一个图片上传的功能。为了贯彻“模块化”的思想,笔者特地将上传模块抽象出来,只要在需要上传的页面include即可。
在Admin模块的View文件夹添加Common文件夹,在Common文件夹下添加upload.html,代码如下:
该段代码与一般代码区别不大,但是重点在于:
uploadCallback && uploadCallback(data.url);
如果当前页面定义了uploadCallback函数,则将上传后的结果回调到该函数。
上传代码,编辑Admin模块下的Index控制器,添加upload方法,代码如下:
public function upload()
{
$upload = new Upload();// 实例化上传类
$upload->maxSize = 1024 * 1024 * 2;// 设置附件上传大小
$upload->exts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = __DIR__ . '/../../../upload/'; // 设置附件上传根目录
$upload->savePath = ''; // 设置附件上传(子)目录
// 上传文件
$info = $upload->upload();
if (!$info)
{
$this->ajaxReturn(array(
'error' => $upload->getError()
));
}
else
{
$path = $upload->rootPath . $info['file']['savepath'] . $info['file']['savename'];
$image = new Image();
$image->open($path);
$image->thumb(200, 200, Image::IMAGE_THUMB_CENTER)->save($path);
$this->ajaxReturn(array(
'url' => U('/', '', false, true) . 'upload/' . $info['file']['savepath'] . $info['file']['savename']
));
}
}
使用时直接使用以下代码引入即可(示例代码在Application/Admin/View/Article/post.html中):
由于回调函数已经写死了“uploadCallback”,所以目前来说该上传组件一个页面只能使用一个。
Admin模块比较重要的功能就是以上列出来的,其他功能基本上都是添加、编辑、列表、删除功能,由于篇幅关系这里不再赘述,有需要的读者可以前往github下载源码:
https://github.com/xialeistudio/thinkphp-inaction/tree/master/blog
12.6.2 Common模块
1. 分类处理
Common模块是公用模块,其他模块公用的功能可以放在该模块下,比如上文中的“文章-分类模型”就是公用Model,所以放在Common/Model下。
博客系统在设计文章分类时有“isNav”字段,该字段用来标识分类是否是导航栏中的分类,所以可以明确出来的需求有:
? 读取属于导航栏的分类(status为1)
? 读取不属于导航栏的分类(status为0)
? 读取全部分类
而以上需求返回值都是一致的,也就是分类列表,所以可以将以上三个需求封装成一个函数,根据传入的status来决定返回数据。
编辑Application/Common/Common/function.php,添加如下代码:
/**
* 获取分类
* @param int $isNav
* @return mixed
*/
function getCategory($isNav = -1)
{
$map = array();
if ($isNav > -1)
{
$map['isNav'] = $isNav;
}
$model = new \Think\Model('Category');
return $model->where($map)->order('sort DESC')->select();
}
该函数对“isNav”参数的处理有个技巧。当给定的stauts大于-1时可以发现添加了一个过滤参数,如果status等于-1则不添加,所以该函数可以实现上文中提到的三个需求。
2. 友情链接列表
博客系统设计了友情链接功能,如果是在控制器中使用Model查询的话,每个需要友情链接的部分都需要查一次数据库,会产生重复代码,所以读取友情链接需要提取函数以供前端调用。
编辑Common/Conf/config.php文件,代码如下:
function getLinks()
{
return M('Link')->where(array('status' => 1))->order('linkId DESC')->select();
}
3. 数据库字段大小写
在使用ThinkPHP的Model进行数据库操作时,返回的数据键名总是大写的。查看ThinkPHP源码发现,ThinkPHP默认的键名是大写,由于ThinkPHP采用PDO链接数据库,可以去看看PDO的链接参数,查看ThinkPHP默认的配置文件convention.php发现,其中有“DB_PARAMS”这个字段,注释为“数据库连接参数”,所以猜测应该是该字段的关系。笔者查资料发现,PDO有“PDO::ATTR_CASE”这个参数来控制大小写。
编辑Common/Conf/config.php,添加数据库配置,代码如下:
'DB_TYPE' => 'mysql',
'DB_DSN' => 'mysql:host=localhost;dbname=thinkphp_blog;charset=utf8mb4',
'DB_uSER' => 'root',
'DB_PWD' => 'root',
'DB_PREFIX' => 'blog_',
'DB_PARAMS' => array(
PDO::ATTR_CASE => PDO::CASE_NATURAL
)
12.6.3 Home模块
1. 前台布局
前台模块公用部分有顶部导航栏以及右边的文章分类,左边为主内容区域,该区域根据访问的页面不同而不同,所以Home模块用到了ThinkPHP的模板布局功能。
打开首页如图12-2所示。
图12-2
编辑Common/Conf/config.php文件,添加以下代码:
'LAYOUT_ON' => true
由于开启模板布局后,ThinkPHP会默认使用名为“layout”的模板,所以需要在Home/View下添加layout.html文件。该文件代码如下:
{:C('site.name')}
{__CONTENT__}
所有分类
$categories2 = getCategory();
该布局文件用到了模板常量,而ThinkPHP自带的模板常量只有__PUBLIC__,所以需要在当前模块单独定义。
编辑Home/Conf/config.php文件,添加如下代码:
'TMPL_PARSE_STRING' => array(
'__VENDOR__' => '/thinkphp-inaction/blog/public/vendor',
'__JS__' => '/thinkphp-inaction/blog/public/home/js',
'__CSS__' => '/thinkphp-inaction/blog/public/home/css',
'__IMAGE__' => '/thinkphp-inaction/blog/public/home/images'
),
由于笔者本地项目是部署在 localhost/thinkphp-inaction中,所以在定义模板常量的时候需要写全,读者可以根据自己项目部署情况来编辑目录地址。
前端资源的目录结构如图12-3所示。
图12-3
笔者在实际项目中也是这套结构,共用部分用vendor目录,然后前端资源分模块管理,这样也可以分模块不同人员一起开发一个项目而不会冲突。
由于导航栏、友情链接、全部分类这几个功能都是公用功能,所以不能在IndexController的index方法中编写读取数据的方法,如果这样,会导致假如不是访问Index/index的时候,导航栏、友情链接、全部分类会读取不到数据而报错。
所以在模板文件中使用调用Common模块中的getCategory和getLinks函数,这样就不会出现读取不到数据的问题了。
调用代码如下:
$categories = getCategory(1);
{$category.name}
请注意“”标签,在模板中运行PHP的话需要使用该标签,变量定义之后在接下来的代码中就可以直接使用ThinkPHP的模板语言进行操作了。
2. 评论间隔处理
由于博客系统的评论采用的是ajax异步评论的方法,如果有人恶意提交接口刷评论而系统不做处理的话,博客系统数据库很可能被写满,所以需要使用缓存来做评论间隔处理。
编辑Home/Controller/IndexController.class.php,添加comment方法,代码如下:
public function comment($id)
{
$model = new Model('Article');
$article = $model->find(array('articleId' => $id));
if (empty($article))
{
$this->error('文章不存在');
}
$key = get_client_ip() . '-view-article-' . $id;
$cache = S($key);
if (!empty($cache))
{
$this->error('评论间隔必须大于1分钟');
}
$nickname = I('nickname');
$content = I('content');
if (empty($nickname))
{
$this->error('昵称不能为空');
}
if (empty($content))
{
$this->error('评论内容不能为空');
}
$data = array(
'nickname' => $nickname,
'content' => $content,
'createdAt' => time(),
'createdIp' => get_client_ip(),
'articleId' => $id
);
$commentModel = new Model('Comment');
if (!$commentModel->data($data)->add())
{
$this->error('评论失败');
}
S($key, 1, 60);
$data['createdAt'] = date('m-d H:i', $data['createdAt']);
$this->ajaxReturn($data);
}
$id为被评论的文章ID,$key = get_client_ip() . '-view-article-' . $id;这段代码使用ID+IP的方式识别当前评论用户,如果S函数返回值不为空,证明缓存有效期内(本代码示例中为1分钟)已经评论过,所以需要返回“评论间隔必须大于1分钟”的错误信息。
如果评论成功,则使用当前$key写入缓存,有效期1分钟。
3. Ajax评论
为了提升用户体验,在文章页评论功能的开发中使用Ajax。打开Home/View/Index/article.html(请先下载源码),看到最下面的部分,代码如下:
在提交的时候使用“$.post”方法提交。在回调函数中需要先判断是否出错,如果出错则显示错误信息,否则显示该评论。显示评论使用的是jQuery的prepend方法,因为最新的评论在最前面,所以需要将生成的html添加到最前面。
12.7 项目总结
由于本博客系统代码量略多,本章只截取经典的、也是常用的功能模块进行重点介绍,希望大家在本项目中多花心思,该项目可以直接上线运行。这也是大家自己动手开发的第一个线上项目,具有个人学习ThinkPHP历程中划时代的意义。
项目已托管至github,项目地址:
https://github.com/xialeistudio/thinkphp-inaction/tree/master/blog
如有任何问题,请提交issues,地址:
https://github.com/xialeistudio/thinkphp-inaction/issues