实战SimpleBlog(一):项目初始化与用户系统搭建

学了这么久 PHP 和 ThinkPHP,是时候做点东西了。这是这个系列的最后一部分,目标很简单:把之前学的东西串起来,亲手做一个能跑起来的博客系统

我管这个博客系统叫 SimpleBlog。为啥选博客?因为它麻雀虽小五脏俱全------用户管理、发文章、评论 ,一个Web应用最核心的几样它都占了。通过做这个,就能把 ThinkPHP 中的路由、控制器、模型、视图这些零件,组装成一个真正能用的东西。和之前一样,文章里不会贴出所有代码 ,因为那样内容太多,而且也不必要。我只会把最关键、最容易卡住的部分 的代码贴出来,比如核心的控制器方法、模型定义。完整的、能直接复制粘贴运行的代码,都会放到 Gitee 代码仓库里。

为了方便大家对照,仓库的代码结构会和文章篇目一一对应。比如这篇是第一篇,你能在仓库里找到一个叫 01 的文件夹,里面就是这期完整的可运行代码。

一、项目需求与规划

二、项目初始化与配置

1、创建项目

2、配置数据库连接

3、数据库设计

三、实现用户注册与登录

1、开启Session

2、创建用户模型与验证器

3、构建控制器与路由

4、编写视图模板

5、实现认证与权限控制

四、效果演示

五、代码仓库


一、项目需求与规划

这个实战项目要做的是一个最基础的博客系统,叫 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 | 评论时间 |

设计说明

  1. 外键是关联核心 :blog_post.user_id 和 blog_comment.post_id、blog_comment.user_id 这些字段,是实现数据关联和权限管理(如"只能修改自己的博客")的基础。
  2. 密码安全 :password 字段存储由 password_hash() 方法生成的哈希值。
  3. 时间记录 :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}

关键点说明

  1. 模板继承 :使用 {extend} 继承基础模板,{block} 定义可替换区块。这使得头部导航、页脚等公共部分只用在模板中进行维护即可。
  2. CSRF防护 :每个表单都包含 {:token()},对表单进行令牌验证,确保表单提交安全。
  3. Bootstrap集成:通过CDN引入Bootstrap 5,使用现成的CSS类快速构建美观响应式界面。
  4. 智能导航栏 :根据 $Request.session.user_id 判断用户登录状态,动态显示不同导航项。
  5. 错误处理 :模板页面顶部通过 alert 组件显示操作结果反馈。
  6. 路由生成:使用 {: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);
}

关键点说明

  1. 中间件的核心逻辑 :中间件的 handle 方法检查 session('?user_id') 是否存在,不存在则拦截请求。这是权限控制的基础。
  2. 智能跳转 :拦截未登录请求时,会记录用户原本想访问的URL(session('redirect_url', $request->url())),登录成功后可以引导用户回到目标页面,提升用户体验。
  3. 路由分组应用 :使用 Route::group()->middleware() 将中间件应用到一组路由,这是最清晰、最易维护的方式。通过路由定义就能一目了然地看出哪些功能需要登录。
  4. 权限控制的扩展性 :当前只做了基础的登录检查。如果后续需要更复杂的权限(如管理员、普通用户角色),可以在这个中间件基础上扩展,比如检查 session('?user_role') 等。

至此,完整的用户系统已经实现:从数据库设计、模型验证、控制器处理、视图展示到最后的权限保护,形成了一个安全、完整的闭环。用户需要先注册、登录,才能访问个人中心和后续的博客管理功能。

四、效果演示

写了这么久,总算可以看看成果了。本篇文章我们搞定了用户系统,现在把项目跑起来,看一下效果。
1. 首页

访问项目地址,顶栏导航是空的,只有"登录"和"注册"两个按钮。


2. 注册

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


3. 登录


4. 个人中心

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

五、代码仓库

仓库地址:

php 复制代码
https://gitee.com/little_z/simple-blog.git

本篇文章对应的代码在 01 目录下。

相关推荐
JaguarJack1 小时前
FrankenPHP 原生支持 Windows 了
后端·php·服务端
BingoGo2 小时前
FrankenPHP 原生支持 Windows 了
后端·php
JaguarJack1 天前
PHP 的异步编程 该怎么选择
后端·php·服务端
BingoGo1 天前
PHP 的异步编程 该怎么选择
后端·php
JaguarJack2 天前
为什么 PHP 闭包要加 static?
后端·php·服务端
ServBay3 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
用户962377954483 天前
CTF 伪协议
php
BingoGo5 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack5 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo6 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php