实战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 目录下。

相关推荐
云游云记14 小时前
php 随机红包数生成
开发语言·php·随机红包
2501_9011478321 小时前
幂函数实现的优化与工程思考笔记
笔记·算法·面试·职场和发展·php
山东布谷网络科技1 天前
对标Yalla和Chamet:海外直播语聊APP中多人派对房的关键技术细节
java·开发语言·人工智能·php·语音识别·软件需求·海外电商系统开发
Sinowintop1 天前
易连EDI-EasyLink之WebEDI功能解读
服务器·microsoft·php·edi·国产edi软件·webedi
China_Yanhy1 天前
入职 Web3 运维日记 · 第 12 日:拥堵的跨链桥 —— 消失的 Gas 与“守护者”脚本
运维·web3·php
BingoGo1 天前
PHP 的问题不在语言本身,而在我们怎么写它
后端·php
行走的陀螺仪1 天前
GitLab + GitLab Runner 本地 Docker 部署实战文档
ci/cd·docker·gitlab·php·gitlab-runner
Re.不晚1 天前
Redis事务
数据库·redis·php
BingoGo1 天前
如何重构遗留 PHP 代码 不至于崩溃
后端·php