学了这么久 PHP 和 ThinkPHP,是时候做点东西了。这是这个系列的最后一部分,目标很简单:把之前学的东西串起来,亲手做一个能跑起来的博客系统。
我管这个博客系统叫 SimpleBlog。为啥选博客?因为它麻雀虽小五脏俱全------用户管理、发文章、评论 ,一个Web应用最核心的几样它都占了。通过做这个,就能把 ThinkPHP 中的路由、控制器、模型、视图这些零件,组装成一个真正能用的东西。和之前一样,文章里不会贴出所有代码 ,因为那样内容太多,而且也不必要。我只会把最关键、最容易卡住的部分 的代码贴出来,比如核心的控制器方法、模型定义。完整的、能直接复制粘贴运行的代码,都会放到 Gitee 代码仓库里。
为了方便大家对照,仓库的代码结构会和文章篇目一一对应。比如这篇是第一篇,你能在仓库里找到一个叫 01 的文件夹,里面就是这期完整的可运行代码。
一、项目需求与规划
这个实战项目要做的是一个最基础的博客系统,叫 SimpleBlog 。目标是:用最少的功能,跑通一个完整流程。
功能模块共有三个,分别是:
1. 用户模块
- 用户注册、登录、退出。
2. 博客模块
- 登录用户可发布、编辑、删除自己的博客(纯文字)。
- 首页展示所有用户发布的博客(按时间倒序)。
- 个人中心展示自己的博客列表(按时间倒序)。
3. 评论模块
- 登录用户可在任意博客下发表评论。
- 博客详情页展示所有关联评论。
目标就一个: 把之前学的 ThinkPHP 核心知识(MVC、模型关联、验证、中间件等)串起来,做出一个真正能运行的程序。
开发原则 : 界面简洁,代码聚焦,"够用就行 "。文章只讲关键代码,完整代码可查看每篇对应的代码仓库(如本篇对应仓库中的 01 部分)。
二、项目初始化与配置
1、创建项目
使用 composer 命令创建 SimpleBlog。在命令行中执行:
bash
composer create-project topthink/think SimpleBlog
因为本项目要使用 thinkTemplate,所以需要安装 think-view 扩展(该扩展会自动安装 think-template 依赖库)。
bash
composer require topthink/think-view
2、配置数据库连接
将 .example.env 文件重命名为 .env,然后根据你的数据库连接进行配置,配置内容说明如下:
bash
APP_DEBUG = true
DB_TYPE = mysql(数据库连接类型,默认mysql)
DB_HOST = 数据库IP
DB_NAME = 数据库名称
DB_USER = 用户名
DB_PASS = 密码
DB_PORT = 数据库端口号
DB_CHARSET = utf8
DB_PREFIX = blog_(数据库表前缀)
DEFAULT_LANG = zh-cn
3、数据库设计
三个功能模块(用户、博客、评论),需要设计三张数据表。它们之间的关系是:
- 一个用户 可以写多篇博客(一对多)
- 一篇博客 可以有多条评论(一对多)
以下是三张表结构设计:
用户表 blog_user:存储用户信息。
|------------|----------------|--------|
| 字段名 | 类型 | 说明 |
| id | INT,主键 | 用户唯一ID |
| username | VARCHAR(64),唯一 | 登录用户名 |
| password | VARCHAR(255) | 加密后的密码 |
| created_at | TIMESTAMP | 账户创建时间 |
| updated_at | TIMESTAMP | 信息更新时间 |
博客表 blog_post:存储博客文章。
|------------|--------------|----------------------|
| 字段名 | 类型 | 说明 |
| id | INT,主键 | 文章唯一ID |
| user_id | INT,外键 | 作者ID,关联 blog_user.id |
| title | VARCHAR(255) | 文章标题 |
| content | TEXT | 文章内容 |
| created_at | TIMESTAMP | 发布时间 |
| updated_at | TIMESTAMP | 最后修改时间 |
评论表 blog_comment:存储对博客的评论。
|------------|--------------|------------------------|
| 字段名 | 类型 | 说明 |
| id | INT,主键 | 评论唯一ID |
| post_id | INT,外键 | 所属文章ID,关联 blog_post.id |
| user_id | INT,外键 | 评论者ID,关联 blog_user.id |
| content | VARCHAR(500) | 评论内容 |
| created_at | TIMESTAMP | 评论时间 |
设计说明:
- 外键是关联核心 :blog_post.user_id 和 blog_comment.post_id、blog_comment.user_id 这些字段,是实现数据关联和权限管理(如"只能修改自己的博客")的基础。
- 密码安全 :password 字段存储由 password_hash() 方法生成的哈希值。
- 时间记录 :created_at 和 updated_at 字段有助于跟踪数据和实现"按时间排序"功能。
完整可运行的SQL文件请查看代码仓库的 init.sql。
三、实现用户注册与登录
项目已经建好并完成初始化,接下来就可以开始写代码了。接下来我们将实现博客的"用户系统"------注册、登录、退出。这是所有功能的前提,我们会完成从页面到数据库的完整流程,并确保密码等关键信息的安全。
1、开启Session
首先我们要开启 Session,因为登录成功后会将用户ID和一些信息存入 Session,并且验证用户是否登录都是通过 Session 来判断,所以第一步我们要先开启 Session。
在全局的中间件定义文件(app/middleware.php)中加上 Session 中间件的定义
php
<?php
// 全局中间件定义文件
return [
// Session初始化
\think\middleware\SessionInit::class
];
2、创建用户模型与验证器
首先,我们创建处理用户数据的两个核心文件:模型(Model) 和验证器(Validator)。模型负责与数据库交互,验证器则确保输入的数据安全合规。
1. 用户模型
在 app/model/ 目录下创建 User.php:
php
<?php
declare (strict_types = 1);
namespace app\model;
use think\Model;
class User extends Model
{
// 设置字段自动写入时间戳(对应数据库的 created_at 等字段)
protected $autoWriteTimestamp = true;
protected $createTime = 'created_at';
protected $updateTime = 'updated_at';
// 在获取和序列化时隐藏密码字段,确保安全
protected $hidden = ['password'];
}
2. 用户验证器
在 app/validate/ 目录下创建 User.php:
php
<?php
declare (strict_types = 1);
namespace app\validate;
use think\Validate;
class User extends Validate
{
// 定义验证规则
protected $rule = [
'username' => 'require|min:3|max:20|unique:user',
'password' => 'require|min:6|confirm',
];
// 定义错误信息
protected $message = [
'username.require' => '用户名不能为空',
'username.min' => '用户名不能少于3个字符',
'username.max' => '用户名不能超过20个字符',
'username.unique' => '用户名已存在',
'password.require' => '密码不能为空',
'password.min' => '密码不能少于6个字符',
'password.confirm' => '两次输入的密码不一致',
];
// 定义用户登录验证分组
public function rulesLogin()
{
return $this->rule([
'username' => 'require',
'password' => 'require',
]);
}
}
关键点说明:
1. 模型的作用 :User 模型继承 think\Model,自动获得了操作 blog_user 表的所有能力(如 User::find())。$hidden 保证了密码字段永远不会被显示。
2. 验证器 :分组(group) 是ThinkPHP验证器非常实用的功能。可以给验证规则定义分组,每个分组的规则彼此独立,但可以共用别名规则和错误信息。它让我们用同一套规则文件,灵活应对注册 (需要确认密码、检查用户名唯一性)和登录(仅校验用户名和密码不能为空)这两种不同需求。
3. 核心安全规则:
- unique:user:在注册时自动检查用户名在数据库中是否已存在。
- confirm :要求提交一个 password_confirm 字段,确保两次输入的密码一致。
现在,数据和规则的基础就打好了。接下来,我们就可以在控制器中使用它们了。
3、构建控制器与路由
现在,我们创建接收请求的"调度中心"(控制器)和定义访问路径(路由),将前端的表单、后台的验证逻辑与数据库操作连接起来。
1. 定义路由
在 route/app.php 中,定义处理用户认证的所有URL入口:
php
<?php
use think\facade\Route;
// 认证相关路由组
Route::group('auth', function () {
// 显示注册页面
Route::get('register', 'Auth/register');
// 处理注册提交
Route::post('register', 'Auth/doRegister')->token();
// 显示登录页面
Route::get('login', 'Auth/login');
// 处理登录提交
Route::post('login', 'Auth/doLogin')->token();
// 处理退出
Route::get('logout', 'Auth/logout');
});
// 需要登录后才能访问的路由组
Route::group(function () {
// 个人中心
Route::get('user', 'User/index');
});
2. 创建认证控制器
在 app/controller/ 目录下创建 AuthController.php(config/route.php 中设置了使用控制器后缀):
php
<?php
namespace app\controller;
use app\BaseController;
use app\model\User;
use app\validate\User as UserValidate;
use think\facade\Request;
class AuthController extends BaseController
{
// 显示注册页面
public function register()
{
return view('auth/register');
}
// 处理注册提交
public function doRegister()
{
$data = Request::only(['username', 'password', 'password_confirm']);
// 验证数据
try {
validate(UserValidate::class)->check($data);
} catch (\think\exception\ValidateException $e) {
// 验证失败,携带错误信息返回注册页
return redirect('/auth/register')->with('errors', $e->getError());
}
// 密码加密
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
// 创建用户
User::create($data);
// 注册成功,跳转到登录页
return redirect('/auth/login')->with('success', '注册成功,请登录');
}
// 显示登录页面
public function login()
{
return view('auth/login');
}
// 处理登录逻辑
public function doLogin()
{
$data = Request::only(['username', 'password']);
// 基础数据验证
try {
validate(UserValidate::class)->check($data, 'login');
} catch (\think\exception\ValidateException $e) {
// 验证失败,携带错误信息返回登录页
return redirect('/auth/login')->with('errors', $e->getError());
}
// 查找用户
$user = User::where('username', $data['username'])->find();
// 验证密码
if (!$user || !password_verify($data['password'], $user->password)) {
return redirect('/auth/login')->with('errors', '用户名或密码错误');
}
// 登录成功,保存会话
session('user_id', $user->id);
session('username', $user->username);
session('created_at', $user->created_at);
// 跳转到个人中心
return redirect('/user/index');
}
// 处理注销逻辑
public function logout()
{
// 清除会话
session(null);
// 跳转到首页
return redirect('/');
}
}
3. 创建用户控制器(个人中心)
在 app/controller/ 目录下创建 UserController.php:
php
<?php
namespace app\controller;
use app\BaseController;
class UserController extends BaseController
{
// 个人中心首页
public function index()
{
// 获取当前登录用户信息
$user = [
'id' => session('user_id'),
'username' => session('username'),
'created_at' => session('created_at'),
];
// 将用户数据传递给视图
return view('user/index', ['user' => $user]);
}
}
关键点说明:
1. 路由分离设计 :采用 GET 请求显示页面,POST 请求处理表单提交。这是RESTful设计的基础,使逻辑更清晰。
2. 控制器方法分工:
- register()/login():仅渲染视图,显示页面。
- doRegister()/doLogin():处理核心业务逻辑,包含验证、数据库操作和Session管理。
3. 密码安全 :注册时使用 password_hash() 加密,登录时使用 password_verify() 验证。
4. Session管理:登录成功后,将用户ID、用户名和创建时间存入Session,退出时清空Session,在其它控制器中通过Session获取当前用户信息。
5. 用户体验 :使用 redirect()->with() 在重定向时携带一次性提示信息(成功或错误),提升交互体验。
4、编写视图模板
这一节,我们创建用户交互的界面。使用ThinkPHP的模板引擎,采用 "继承" 的方式组织代码,让页面结构清晰且易于维护。
1. 创建基础布局模板
在 view 目录下创建 layout.html,作为所有页面的"骨架":
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{block name="title"}SimpleBlog{/block}</title>
<!-- 引入 Bootstrap 5 CSS -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
{block name="style"}{/block}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<div class="container">
<a class="navbar-brand" href="{:url('/')}">SimpleBlog</a>
<div class="navbar-nav ms-auto">
{if $Request.session.user_id}
<span class="nav-item nav-link">欢迎,{$Request.session.username}</span>
<a class="nav-item nav-link" href="{:url('user/index')}">个人中心</a>
<a class="nav-item nav-link" href="{:url('auth/logout')}">退出</a>
{else}
<a class="nav-item nav-link" href="{:url('auth/login')}">登录</a>
<a class="nav-item nav-link" href="{:url('auth/register')}">注册</a>
{/if}
</div>
</div>
</nav>
<!-- 主内容区 -->
<main class="container">
<!-- 消息提示区 -->
{if $Request.session.success}
<div class="alert alert-success alert-dismissible fade show" role="alert">
{$Request.session.success}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{/if}
{if $Request.session.errors}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{$Request.session.errors}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{/if}
<!-- 页面具体内容 -->
{block name="content"}默认内容{/block}
</main>
<!-- 引入 Bootstrap JS -->
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
{block name="script"}{/block}
</body>
</html>
2. 创建注册页面模板
在 view/auth 目录下创建 register.html:
html
{extend name="layout"}
{block name="title"}用户注册 - SimpleBlog{/block}
{block name="content"}
<div class="row justify-content-center">
<div class="col-md-6">
<h2 class="mb-4">用户注册</h2>
<form method="post" action="{:url('auth/doRegister')}">
<!-- CSRF令牌(防跨站攻击) -->
<input type="hidden" name="__token__" value="{:token()}" />
<!-- 用户名 -->
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" value="{$old.username|default=''}" required>
</div>
<!-- 密码 -->
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<!-- 确认密码 -->
<div class="mb-3">
<label for="password_confirm" class="form-label">确认密码</label>
<input type="password" class="form-control {if isset($errors['password_confirm'])}is-invalid{/if}"
id="password_confirm" name="password_confirm" required>
</div>
<button type="submit" class="btn btn-primary w-100">注册</button>
<div class="mt-3 text-center">
已有账号?<a href="{:url('auth/login')}">立即登录</a>
</div>
</form>
</div>
</div>
{/block}
3. 创建登录页面模板
在 view/auth 目录下创建 login.html:
html
{extend name="layout"}
{block name="title"}用户登录 - SimpleBlog{/block}
{block name="content"}
<div class="row justify-content-center">
<div class="col-md-6">
<h2 class="mb-4">用户登录</h2>
<form method="post" action="{:url('auth/doLogin')}">
<!-- CSRF令牌 -->
<input type="hidden" name="__token__" value="{:token()}" />
<!-- 用户名 -->
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<!-- 密码 -->
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">登录</button>
<div class="mt-3 text-center">
还没有账号?<a href="{:url('auth/register')}">立即注册</a>
</div>
</form>
</div>
</div>
{/block}
4. 创建个人中心模板
在 view/user 目录下创建 index.html:
html
{extend name="layout"}
{block name="title"}个人中心 - SimpleBlog{/block}
{block name="content"}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4>个人中心</h4>
</div>
<div class="card-body">
<p><strong>用户名:</strong> {$user.username}</p>
<p><strong>用户ID:</strong> {$user.id}</p>
<p><strong>注册时间:</strong> {$user.created_at|default='刚刚'}</p>
<div class="mt-4">
<a href="{:url('post/create')}" class="btn btn-success me-2">发布新博客</a>
<a href="{:url('post/my')}" class="btn btn-outline-primary">我的博客</a>
</div>
</div>
</div>
</div>
</div>
{/block}
关键点说明:
- 模板继承 :使用 {extend} 继承基础模板,{block} 定义可替换区块。这使得头部导航、页脚等公共部分只用在模板中进行维护即可。
- CSRF防护 :每个表单都包含 {:token()},对表单进行令牌验证,确保表单提交安全。
- Bootstrap集成:通过CDN引入Bootstrap 5,使用现成的CSS类快速构建美观响应式界面。
- 智能导航栏 :根据 $Request.session.user_id 判断用户登录状态,动态显示不同导航项。
- 错误处理 :模板页面顶部通过 alert 组件显示操作结果反馈。
- 路由生成:使用 {:url('路由名')} 智能生成URL,便于后期路由调整。
至此,用户从访问网站到完成注册、登录,再到进入个人中心的完整界面流程就全部完成了。
5、实现认证与权限控制
现在,我们的系统中有一个问题,就是用户没有登录也可以访问个人中心,所以接下来我们要做的就是确保只有登录用户才能访问特定页面,并统一管理权限验证逻辑。该功能我们使用**中间件(Middleware)**来实现。
1. 创建认证中间件
在项目根目录执行命令生成中间件文件:
bash
php think make:middleware Auth
编辑生成的 app/middleware/Auth.php 文件:
php
<?php
declare (strict_types = 1);
namespace app\middleware;
class Auth
{
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @return Response
*/
public function handle($request, \Closure $next)
{
// 检查session中是否存在用户ID
if (!session('?user_id')) {
// 普通请求,重定向到登录页
// 记录当前访问地址,登录后可以跳转回来
session('redirect_url', $request->url());
// 如果不存在用户ID,重定向到登录页面
return redirect((string) url('auth/login'))->with('errors', '请先登录后访问此页面');;
}
// 如果存在用户ID,继续执行下一个中间件或路由处理
return $next($request);
}
}
2. 应用中间件到路由
修改 route/app.php,将中间件应用到需要保护的路由:
php
<?php
use think\facade\Route;
use app\middleware\Auth;
// ... 前面已存在的路由定义 ...
// 需要登录后才能访问的路由组
Route::group(function () {
// 个人中心
Route::get('user', 'User/index');
})->middleware(Auth::class); // 应用Auth中间件到整个路由组
3. 处理登录逻辑修改
修改 app/controller/AuthController.php,判断Session中是否记录有访问地址,如果有,则向该地址跳转:
php
// 处理登录逻辑
public function doLogin()
{
// ...代码不变...
// 检查是否有记录跳转地址,有则跳转,否则跳转到个人中心
$redirectUrl = '/user/index';
if (session('?redirect_url')) {
$redirectUrl = session('redirect_url');
session('redirect_url', null); // 清除记录
}
return redirect((string) $redirectUrl);
}
关键点说明:
- 中间件的核心逻辑 :中间件的 handle 方法检查 session('?user_id') 是否存在,不存在则拦截请求。这是权限控制的基础。
- 智能跳转 :拦截未登录请求时,会记录用户原本想访问的URL(session('redirect_url', $request->url())),登录成功后可以引导用户回到目标页面,提升用户体验。
- 路由分组应用 :使用 Route::group()->middleware() 将中间件应用到一组路由,这是最清晰、最易维护的方式。通过路由定义就能一目了然地看出哪些功能需要登录。
- 权限控制的扩展性 :当前只做了基础的登录检查。如果后续需要更复杂的权限(如管理员、普通用户角色),可以在这个中间件基础上扩展,比如检查 session('?user_role') 等。
至此,完整的用户系统已经实现:从数据库设计、模型验证、控制器处理、视图展示到最后的权限保护,形成了一个安全、完整的闭环。用户需要先注册、登录,才能访问个人中心和后续的博客管理功能。
四、效果演示
写了这么久,总算可以看看成果了。本篇文章我们搞定了用户系统,现在把项目跑起来,看一下效果。
1. 首页
访问项目地址,顶栏导航是空的,只有"登录"和"注册"两个按钮。

2. 注册
点"注册",填个用户名和密码,密码输短了会有红色提示,两次不一致也会报错。这些错误提示都是验证器里自定义的。

3. 登录

4. 个人中心
点"个人中心",页面显示用户名、ID、注册时间。这里已经应用了中间件,如果没登录直接访问 /user,会被踢回登录页并提示"请先登录后访问此页面"。

五、代码仓库
仓库地址:
php
https://gitee.com/little_z/simple-blog.git
本篇文章对应的代码在 01 目录下。