88. 边栏资源推荐
简介
在本节里,我们完成话题列表和分类话题列表页右侧边栏的资源推荐功能。
需求分解
在本节里,我们需要完成推荐资源的前后台所有功能开发。
后台模块
- 有权限的管理员用户才可以访问 CURD 页面;
- 考虑到推荐资源不会很多,所以列表页不需要搜索和分页显示,一次列出所有记录;
- 添加和编辑时,资源标题不能为空、必须唯一并且长度在 3-50 个字符之间,资源链接不能为空、必须是 URL 格式并且长度不超过 100 个字符;
- 和其它控制器请求方式保持一致,创建表单提交是 POST 请求、编辑表单提交是 PUT 请求,删除操作是 DELETE 请求;
- 点击删除用户时需要弹出确认提示,当管理员确认后再能删除;
- 考虑到推荐资源更新频率比较低,为了减少数据库读写次数和服务器访问压力,在前台展示时会缓存数据,所以进行添加、编辑和删除操作后需要清空前台已有缓存。
前台模块
- 在话题列表和分类话题列表页右侧边栏显示推荐资源列表;
- 读取推荐资源列表时,优先使用已有缓存数据。
数据字典
字段名称 | 描述 | 字段类型 | 加索引缘由 | 其他 |
---|---|---|---|---|
titie |
标题 | 字符串(string) | 不需要 | 'default => 0, 'limit' => 50 |
url |
链接地址 | 字符串(string) | 不需要 | 'default => 0, 'limit' => 200 |
default => xx
—— 为字段添加默认值;limit => xx
—— 为字符串字段指定最大长度。
数据迁移
我们为资源推荐模型取名 Link
, 首先我们创建数据库表迁移:
$ php think migrate:create CreateTableLink
在迁移文件里我们先为数据表插入几条默认的推荐资源,完整代码如下:
database/migrations/xxxxxxxxxxxxxx_create_table_link.php
<?php
use think\migration\Migrator;
use think\migration\db\Column;
use app\common\model\Link;
class CreateTableLink extends Migrator
{
public function up()
{
$table = $this->table('link',array('engine'=>'InnoDB'));
$table->addColumn('title', 'string', array('limit' => 50, 'default' => '', 'null' => false, 'comment' => '标题'))
->addColumn('url', 'string', array('default' => '', 'null' => true, 'comment' => '链接'))
->addColumn('create_time', 'integer', array('default' => 0, 'signed' => false, 'null' => true))
->addColumn('update_time', 'integer', array('default' => 0, 'signed' => false, 'null' => true))
->save();
$current_time = time();
$links = [
[
'title' => 'ThinkPHP官网',
'url' => 'http://www.thinkphp.cn/',
'create_time' => $current_time,
'update_time' => $current_time,
], [
'title' => '看云官网',
'url' => 'https://www.kancloud.cn/explore',
'create_time' => $current_time,
'update_time' => $current_time,
], [
'title' => 'Laravel China社区',
'url' => 'https://learnku.com/laravel',
'create_time' => $current_time,
'update_time' => $current_time,
],
];
Link::insertAll($links);
}
public function down()
{
$this->dropTable('link');
}
}
注意: 因为我们在迁移文件里使用 Link
模型来添加初始化数据,所以当我们完成 Link
数据库模型声明后再执行迁移命令生成数据库表。
验证器
接下来我们使用命令行工具创建推荐资源的验证器:
$ php think make:validate common/Link
验证器代码如下:
application/common/validate/Link.php
<?php
namespace app\common\validate;
use think\Validate;
class Link extends Validate
{
protected $rule = [
'title' => 'require|length:3,50|unique:link',
'url' => 'require|max:100|url',
];
protected $message = [
'title.require' => '标题不能为空',
'title.length' => '标题不能少于3个字符',
'title.unique' => '当前标题已存在',
'url.require' => '资源链接不能为空',
'url.max' => '资源链接不能超过100个字符',
'url.url' => '资源链接必须是URL',
];
}
观察者
因为推荐资源更新概率比较小,而话题列表访问频率比较大,所以我们在查询推荐资源时会缓存一段时间,但我们还需要创建一个观察者来监听推荐资源创建、更新或删除事件使当前缓存数据失效。
application/common/observer/Link.php
<?php
namespace app\common\observer;
use app\common\model\Link as LinkModel;
class Link
{
public function afterWrite(LinkModel $topic)
{
LinkModel::clearCached();
}
public function afterDelete(LinkModel $topic)
{
LinkModel::clearCached();
}
}
数据模型
使用命令行工具创建推荐资源的数据模型:
$ php think make:model common/Link
数据模型代码如下:
application/common/model/Link.php
<?php
namespace app\common\model;
use think\Model;
use think\facade\Cache;
use app\common\validate\Link as Validate;
use app\common\exception\ValidateException;
class Link extends Model
{
// 缓存主键
protected const CACHE_KEY = 'links';
// 缓存有效时长(秒)
protected const CACHE_SECONDS = 1440 * 60;
protected static function init()
{
self::observe(\app\common\observer\Link::class);
}
/**
* 查询出所有资源数据并缓存
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-04
* @return array [description]
*/
public static function selectAll()
{
$links = Cache::store('redis')->get(self::CACHE_KEY);
if(!empty($links)){
// 当缓存有数据时直接返回缓存数据
return $links;
}
$links = self::order('id', 'ASC')->select();
// 当查询结果写入缓存
Cache::store('redis')->set(self::CACHE_KEY, $links, self::CACHE_SECONDS);
return $links;
}
/**
* 清除缓存数据
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-04
* @return [type] [description]
*/
public static function clearCached()
{
Cache::store('redis')->rm(self::CACHE_KEY);
}
/**
* 后台模块搜索方法
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-28
* @param array $params [description]
* @param integer $page_rows [description]
* @return [type] [description]
*/
public static function adminPaginate($params = [], $page_rows = 15)
{
$self = self::order('id', 'ASC');
$map = [];
foreach ($params as $name => $text) {
$text = trim($text);
switch ($name) {
case 'keyword':
if(!empty($text)){
$like_text = '%'.$text.'%';
$self = $self->whereLike('title', $like_text);
}
break;
}
}
return $self->paginate($page_rows, false, ['query' => $params]);
}
/**
* 创建记录
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-21
* @param array $data 表单提交数据
* @return Topic [description]
*/
public static function createItem($data)
{
$validate = new Validate;
if(!$validate->batch(true)->check($data)){
$e = new ValidateException('数据验证失败');
$e->setData($validate->getError());
throw $e;
}
try{
$link = new self;
$link->allowField(true)->save($data);
}catch (\Exception $e){
throw new \Exception('创建资源链接失败');
}
return $link;
}
/**
* 更新记录
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-21
* @param array $data [description]
* @return [type] [description]
*/
public function updateInfo($data)
{
$data['id'] = $this->id;
$validate = new Validate;
if(!$validate->batch(true)->check($data)){
$e = new ValidateException('数据验证失败');
$e->setData($validate->getError());
throw $e;
}
$this->allowField(true)->save($data, ['id' => $this->id]);
return $this;
}
}
接下来,我们运行迁移命令完成数据表的创建:
$ php think migrate:run
后台管理
控制器
首先,我们在后台模块完成推荐资源管理:
$ php think make:controller admin/Link
在后台我们要完成推荐资源的 CURD 操作,所以完整代码如下:
application/admin/controller/Link.php
<?php
namespace app\admin\controller;
use think\Request;
use think\facade\Session;
use tpadmin\controller\Controller;
use app\common\model\Link as LinkModel;
use app\common\exception\ValidateException;
class Link extends Controller
{
public function index(Request $request)
{
$param = $request->param();
$paginate = LinkModel::adminPaginate($param);
$this->assign('param', $param);
$this->assign('paginate', $paginate);
return $this->fetch('link/index');
}
public function create()
{
$this->assign('link', []);
return $this->fetch('link/form');
}
public function save(Request $request)
{
if(!$request->isAjax()){
$this->redirect('[admin.link.create]');
}
try{
$data = $request->post();
$link = LinkModel::createItem($data);
}catch (ValidateException $e){
return $this->error($e->getMessage(), '', ['errors' => $e->getData()]);
}catch (\Exception $e){
return $this->error($e->getMessage());
}
$message = '创建成功';
Session::flash('success', $message);
$this->success($message, url('[admin.link.index]'));
}
public function edit($id)
{
$link = LinkModel::find($id);
$message = null;
if(empty($link)){
$message = '编辑资源不存在';
}
if(!empty($message)){
Session::flash('alert', $message);
$this->redirect('[admin.link.index]');
}
$this->assign('link', $link);
return $this->fetch('link/form');
}
public function update(Request $request, $id)
{
if(!$request->isAjax()){
$this->redirect(url('[admin.link.edit]', ['id' => $id]));
}
$link = LinkModel::find($id);
if(empty($link)){
$this->error('编辑资源不存在', '[admin.link.index]');
}
try{
$data = $request->post();
$link->updateInfo($data);
}catch (ValidateException $e){
$this->error($e->getMessage(), '', ['errors' => $e->getData()]);
}catch (\Exception $e){
$this->error($e->getMessage());
}
$message = '更新成功';
Session::flash('success', $message);
$this->success($message, url('[admin.link.index]'));
}
public function delete($id)
{
$link = LinkModel::find($id);
if(empty($link)){
$this->error('删除资源不存在', '[admin.link.index]');
}
$link->delete();
$message = '删除成功';
Session::flash('success', $message);
$this->success($message, '[admin.link.index]');
}
}
路由
在配置文件里定义控制方法访问路由规则:
route/admin_content.php
<?php
Route::group([
'name' => 'admin',
'middleware' => ['tpadmin.admin', 'tpadmin.admin.role'],
], function () {
.
.
.
// 资源管理
Route::post('link', 'Link@save')->name('admin.link.save');
Route::get('link/create', 'Link@create')->name('admin.link.create');
Route::get('link/<id>/edit', 'Link@edit')->name('admin.link.edit');
Route::put('link/<id>', 'Link@update')->name('admin.link.update');
Route::delete('link/<id>', 'Link@delete')->name('admin.link.delete');
Route::get('link', 'Link@index')->name('admin.link.index');
})->prefix('\\app\\admin\\controller\\');
视图模板
- 资源列表页:
application/admin/view/link/index.html
{extend name="layout:base" /}
{block name="main_content"}
<div class="row maintop">
<div class="col-xs-10 col-sm-5">
<form name="form_search" class="form-search" method="get">
<div class="input-group">
<span class="input-group-addon">
<i class="ace-icon fa fa-check"></i>
</span>
<input type="text" name="keyword" id="keyword" class="form-control" value="{$param.keyword|default=''}" placeholder="请输入关键词" />
<span class="input-group-btn">
<button type="submit" class="btn btn-purple btn-sm">
<span class="ace-icon fa fa-search icon-on-right bigger-110"></span>
搜索
</button>
</span>
</div>
</form>
</div>
<div class="col-xs-4 col-sm-5">
<div class="input-group-btn">
<a href="{:url('[admin.link.index]')}">
<button type="button" class="btn btn-sm btn-purple">
<span class="ace-icon fa fa-globe icon-on-right bigger-110"></span>
显示全部
</button>
</a>
</div>
</div>
<div class="col-xs-4 col-sm-2">
{if auth_check('link/create', $current_adminer->id) }
<a href="{:url('[admin.link.create]')}">
<button class="btn btn-sm btn-primary">
<i class="ace-icon fa fa-plus bigger-110"></i>添加资源
</button>
</a>
{/if}
</div>
</div>
<div class="row">
<div class="col-xs-12">
<table class="table table-striped table-bordered table-hover" id="dynamic-table">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>URL</th>
<th>创建时间</th>
<th class="center">操作</th>
</tr>
</thead>
<tbody>
{foreach $paginate->all() as $key => $link }
<tr>
<td>{$link->id}</td>
<td>{$link->title}</td>
<td>
<a href="{$link->url}" target="_blank">
{$link->url}
</a>
</td>
<td>{$link->create_time}</td>
<td class="center">
{if auth_check('link/edit', $current_adminer->id) }
<a class="green" href="{:url('[admin.link.edit]', ['id' => $link.id])}">
<i class="ace-icon fa fa-pencil bigger-130"></i>编辑
</a>
{/if}
{if auth_check('link/delete', $current_adminer->id) }
<a class="red" herf="javascript:void(0);" onclick="alert_del(this);" data-del-href="{:url('[admin.link.delete]', ['id' => $link.id])}" data-del-id='{$link.id}'>
<i class="ace-icon fa fa-trash-o bigger-130"></i>删除
</a>
{/if}
</td>
</tr>
{/foreach}
</tbody>
</table>
<div>
<?php echo($paginate->render()); ?>
</div>
</div>
</div>
{/block}
- 资源创建或编辑表单页
application/admin/view/link/form.html
{extend name="layout:base" /}
{block name="main_content"}
<?php if(isset($link['id'])): ?>
<form class="form-horizontal adminform" id="model-form" method="post" action="{:url('[admin.link.edit]', ['id' => $link.id])}">
<input type="hidden" name="_method" value="PUT">
<?php else: ?>
<form class="form-horizontal adminform" id="model-form" method="post" action="{:url('[admin.link.save]')}">
<?php endif; ?>
<div class="form-group">
<label class="col-sm-2 control-label no-padding-right" for="form-field-1">
标题:<span class="red">*</span>
</label>
<div class="col-sm-10">
<input type="text" name="title" placeholder="例:ThinkPHP官网" class="col-xs-10 col-sm-5" value="{$link.title|default=''}" required/>
</div>
</div>
<div class="space-4"></div>
<div class="form-group">
<label class="col-sm-2 control-label no-padding-right" for="form-field-1">
URL:<span class="red">*</span>
</label>
<div class="col-sm-10">
<input type="text" name="url" placeholder="例:http://www.thinkphp.cn" class="col-xs-10 col-sm-5" value="{$link.url|default=''}" required/>
</div>
</div>
<div class="space-4"></div>
<div class="clearfix form-actions">
<div class="col-md-offset-3 col-md-9">
<button class="btn btn-info" type="submit">
<i class="ace-icon fa fa-check bigger-110"></i>保存
</button>
<button class="btn" type="reset">
<i class="ace-icon fa fa-undo bigger-110"></i>重置
</button>
</div>
</div>
</form>
{/block}
{block name="page_js"}
<script src="/static/assets/plugins/jquery-validate/jquery.validate.min.js"></script>
<script type="text/javascript">
jQuery(function($){
validAndSubmitForm(
"form#model-form",
{
"title":{
required: true,
minlength: 3,
maxlength: 50
}, "url":{
required: true,
maxlength: 100,
url: true
}
},{
"title":{
required: "标题不能为空",
minlength: "标题不能少于3个字符",
maxlength: "标题不能超过50个字符"
}, "url":{
required: "资源链接不能为空",
maxlength: "资源链接不能超过100个字符",
url: "资源链接必须是URL"
}
}
);
});
</script>
{/block}
权限管理
用 admin
账号登录后台,添加以下路由规则并给 运营 角色组分配访问权限。
效果预览
用 operator
账号登录查看用户管理功能。
前台模块
因为我们需要在话题列表页和分类话题列表页都的右侧都要显示出『活跃用户』,所以我们在两个控制器里添加读取『推荐资源』数据并输入到视图模板。
- 话题列表页
application/index/controller/Topic.php
<?php
.
.
.
use app\common\model\Link as LinkModel;
class Topic extends Base
{
.
.
.
public function index(Request $request)
{
.
.
.
$this->assign('links', LinkModel::selectAll());
return $this->fetch('index');
}
}
- 分类话题列表页
application/index/controller/Category.php
<?php
.
.
.
use app\common\model\Link as LinkModel;
class Category extends Base
{
public function read(Request $request, $id)
{
.
.
.
$this->assign('links', LinkModel::selectAll());
return $this->fetch('topic/index');
}
}
视图模板
上面我们在控制器方法里已经读取到了活跃用户数据,所以接下来我们在左边栏中把数据显示出来:
application/index/view/topic/_sidebar.html
.
.
.
{notempty name='links'}
<div class="card mt-4">
<div class="card-body pt-2">
<div class="text-center mt-1 mb-0 text-muted">资源推荐</div>
<hr class="mt-2 mb-3">
{volist name='links' id='link'}
<a class="media mt-1" href="{$link->url}" target="_blank">
<div class="media-body">
<span class="media-heading text-muted">{$link->title}</span>
</div>
</a>
{/volist}
</div>
</div>
{/notempty}
效果预览
现在我们已经完成了资源管理前后台模块的所有业务开发,现在在后台编辑一条推荐资源信息,刷新前台话题列表页可以看到右侧「推荐资源」也已经立即更新。
Git 版本控制
下面把代码纳入到版本管理:
$ git add -A
$ git commit -m "前端推荐资源"
推荐文章: