详解 ThinkPHP6 中 Queue 与 Event 的结合实践:用户注册场景实现

一、技术背景与优势

1.1 为什么需要队列与事件结合?

  • 同步执行的问题:注册后若直接同步发送邮件、记录日志,会增加接口响应时间(尤其邮件发送可能涉及网络请求)
  • 代码耦合的困扰:将所有后续操作写在注册方法中,会导致代码臃肿,难以维护
  • 可扩展性差:新增注册后操作需修改核心注册代码,不符合开闭原则

1.2 技术组合优势

  • Event 事件机制:实现核心业务与后续操作的解耦,注册流程无需关心后续处理
  • Queue 队列系统:将耗时操作异步化,提升接口响应速度
  • 可靠性保障:队列任务支持失败重试,确保关键操作不丢失

二、环境准备

2.1 安装必要扩展

bash 复制代码
# 安装ThinkPHP6队列扩展
composer require topthink/think-queue

2.2 队列配置

config/queue.php中配置队列驱动(以 Redis 为例):

dart 复制代码
return [
    'default'     => 'redis',
    'connections' => [
        'redis' => [
            'type'       => 'redis',
            'queue'      => 'default',
            'host'       => '127.0.0.1',
            'port'       => 6379,
            'password'   => '',
            'select'     => 0,
            'timeout'    => 0,
            'persistent' => false,
        ],
    ],
    // 失败任务配置
    'failed' => [
        'type'  => 'database',
        'table' => 'queue_failed_jobs',
    ],
];

三、数据库设计(注册日志表)

3.1 创建迁移文件

go 复制代码
php think make:migration create_registration_log_table

3.2 编写迁移代码

php 复制代码
<?php
use think\migration\Migrator;
use think\migration\db\Column;

class CreateRegistrationLogTable extends Migrator
{
    public function change()
    {
        $table = $this->table('registration_log', ['engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci']);
        $table->addColumn('user_id', 'integer', ['comment' => '用户ID'])
              ->addColumn('username', 'string', ['limit' => 50, 'comment' => '用户名'])
              ->addColumn('register_time', 'datetime', ['comment' => '注册时间'])
              ->addColumn('ip', 'string', ['limit' => 50, 'comment' => '注册IP'])
              ->addColumn('user_agent', 'text', ['null' => true, 'comment' => '用户代理'])
              ->addColumn('created_at', 'datetime', ['null' => true, 'comment' => '创建时间'])
              ->setComment('用户注册日志表')
              ->create();
    }
}

3.3 执行迁移

arduino 复制代码
php think migrate:run

四、核心代码实现

4.1 定义用户注册事件

创建app/event/UserRegistered.php

php 复制代码
<?php
namespace app\event;

// 用户注册事件类
class UserRegistered
{
    public $user; // 存储用户信息
    public $registerInfo; // 存储注册相关信息(IP、时间等)

    public function __construct($user, $registerInfo)
    {
        $this->user = $user;
        $this->registerInfo = $registerInfo;
    }
}

4.2 创建队列任务类

4.2.1 邮件发送任务(app/job/SendWelcomeEmail.php

php 复制代码
<?php
namespace app\job;

use think\queue\Job;
use think\facade\Log;

class SendWelcomeEmail
{
    /**
     * 执行队列任务
     */
    public function fire(Job $job, $data)
    {
        // 从数据中获取用户信息
        $user = $data['user'];
        
        // 执行发送邮件逻辑
        $isSuccess = $this->sendEmail($user);
        
        if ($isSuccess) {
            // 任务执行成功,删除任务
            $job->delete();
            Log::info("欢迎邮件发送成功,用户ID:{$user['id']}");
        } else {
            // 处理失败情况
            if ($job->attempts() > 3) {
                // 超过3次重试,记录失败日志
                $job->delete();
                Log::error("欢迎邮件发送失败(超过重试次数),用户ID:{$user['id']}");
            } else {
                // 10秒后重试
                $job->release(10);
            }
        }
    }
    
    /**
     * 实际发送邮件的方法
     */
    private function sendEmail($user)
    {
        // 这里是邮件发送逻辑
        // 示例:使用邮件类发送欢迎邮件
        try {
            // 实际项目中替换为真实的邮件发送代码
            /*
            $mail = new \PHPMailer();
            $mail->setFrom('welcome@example.com', '系统通知');
            $mail->addAddress($user['email'], $user['username']);
            $mail->Subject = '欢迎注册我们的网站';
            $mail->Body = "亲爱的{$user['username']},欢迎注册XXX网站,祝您使用愉快!";
            return $mail->send();
            */
            
            // 测试环境直接返回成功
            return true;
        } catch (\Exception $e) {
            Log::error("邮件发送失败:{$e->getMessage()}");
            return false;
        }
    }
}

4.2.2 注册日志记录任务(app/job/LogRegistration.php

php 复制代码
<?php
namespace app\job;

use think\queue\Job;
use think\facade\Db;
use think\facade\Log;

class LogRegistration
{
    /**
     * 执行队列任务
     */
    public function fire(Job $job, $data)
    {
        // 从数据中获取相关信息
        $user = $data['user'];
        $registerInfo = $data['registerInfo'];
        
        // 执行日志记录
        $isSuccess = $this->recordLog($user, $registerInfo);
        
        if ($isSuccess) {
            $job->delete();
            Log::info("注册日志记录成功,用户ID:{$user['id']}");
        } else {
            // 日志记录失败处理
            if ($job->attempts() > 3) {
                $job->delete();
                Log::error("注册日志记录失败,用户ID:{$user['id']}");
            } else {
                $job->release(5); // 5秒后重试
            }
        }
    }
    
    /**
     * 记录注册日志到数据库
     */
    private function recordLog($user, $registerInfo)
    {
        try {
            Db::name('registration_log')->insert([
                'user_id' => $user['id'],
                'username' => $user['username'],
                'register_time' => $registerInfo['time'],
                'ip' => $registerInfo['ip'],
                'user_agent' => $registerInfo['user_agent'],
                'created_at' => date('Y-m-d H:i:s')
            ]);
            return true;
        } catch (\Exception $e) {
            Log::error("日志记录失败:{$e->getMessage()}");
            return false;
        }
    }
}

4.3 创建事件监听器

4.3.1 邮件发送监听器(app/listener/SendWelcomeEmailListener.php

php 复制代码
<?php
namespace app\listener;

use app\event\UserRegistered;
use think\facade\Queue;
use app\job\SendWelcomeEmail;

class SendWelcomeEmailListener
{
    public function handle(UserRegistered $event)
    {
        // 将任务推送到email队列
        Queue::push(SendWelcomeEmail::class, [
            'user' => $event->user
        ], 'email');
    }
}

4.3.2 日志记录监听器(app/listener/LogRegistrationListener.php

php 复制代码
<?php
namespace app\listener;

use app\event\UserRegistered;
use think\facade\Queue;
use app\job\LogRegistration;

class LogRegistrationListener
{
    public function handle(UserRegistered $event)
    {
        // 将任务推送到log队列
        Queue::push(LogRegistration::class, [
            'user' => $event->user,
            'registerInfo' => $event->registerInfo
        ], 'log');
    }
}

4.4 注册事件与监听器

config/event.php中配置事件监听关系:

dart 复制代码
return [
    'listen' => [
        // 用户注册事件对应的监听器
        'app\event\UserRegistered' => [
            'app\listener\SendWelcomeEmailListener',
            'app\listener\LogRegistrationListener',
        ],
    ],
];

4.5 实现用户注册控制器

创建app/controller/UserController.php

php 复制代码
<?php
namespace app\controller;

use think\facade\Request;
use think\facade\Event;
use think\facade\Db;
use think\facade\Log;

class UserController
{
    /**
     * 用户注册接口
     */
    public function register()
    {
        // 获取注册数据
        $data = Request::only(['username', 'email', 'password']);
        
        // 简单验证
        if (empty($data['username']) || empty($data['email']) || empty($data['password'])) {
            return json(['code' => 1001, 'msg' => '参数不完整']);
        }
        
        try {
            // 密码加密
            $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
            $data['created_at'] = date('Y-m-d H:i:s');
            
            // 保存用户信息
            $userId = Db::name('user')->insertGetId($data);
            $user = Db::name('user')->find($userId);
            
            // 收集注册相关信息
            $registerInfo = [
                'ip' => Request::ip(),
                'user_agent' => Request::server('HTTP_USER_AGENT'),
                'time' => $data['created_at']
            ];
            
            // 触发用户注册事件
            Event::trigger(new \app\event\UserRegistered($user, $registerInfo));
            
            return json([
                'code' => 0,
                'msg' => '注册成功',
                'data' => ['user_id' => $userId]
            ]);
            
        } catch (\Exception $e) {
            Log::error("注册失败:{$e->getMessage()}");
            return json(['code' => 1002, 'msg' => '注册失败']);
        }
    }
}

五、运行与测试

5.1 启动队列监听

arduino 复制代码
# 启动邮件队列监听
php think queue:listen --queue email

# 启动日志队列监听(新终端)
php think queue:listen --queue log

5.2 测试注册流程

通过 Postman 或 curl 工具调用注册接口:

ruby 复制代码
curl -X POST http://yourdomain.com/user/register \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","email":"test@example.com","password":"123456"}'

5.3 验证结果

  1. 检查用户表是否新增用户记录
  2. 检查注册日志表是否有对应记录
  3. 查看队列监听终端输出,确认任务执行情况
  4. 检查日志文件(runtime/log目录)验证执行结果

六、扩展与优化

6.1 失败任务处理

  1. 创建失败任务表:
arduino 复制代码
php think queue:failed-table
php think migrate:run
  1. 查看失败任务:
arduino 复制代码
php think queue:failed
  1. 重试失败任务:
ruby 复制代码
# 重试指定ID的任务
php think queue:retry 1

# 重试所有失败任务
php think queue:retry all

6.2 生产环境部署

在生产环境中,建议使用 Supervisor 管理队列进程:

  1. 安装 Supervisor:
arduino 复制代码
apt-get install supervisor
  1. 创建配置文件(/etc/supervisor/conf.d/thinkphp-queue.conf):
ini 复制代码
[program:thinkphp-queue-email]
command=php /path/to/your/project/think queue:work --queue email
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/thinkphp-queue-email.log

[program:thinkphp-queue-log]
command=php /path/to/your/project/think queue:work --queue log
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/thinkphp-queue-log.log
  1. 启动 Supervisor:
sql 复制代码
supervisorctl reread
supervisorctl update
supervisorctl start thinkphp-queue-*
相关推荐
程序员爱钓鱼5 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
PineappleCoder6 小时前
工程化必备!SVG 雪碧图的最佳实践:ID 引用 + 缓存友好,无需手动算坐标
前端·性能优化
JIngJaneIL6 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
敲敲了个代码6 小时前
隐式类型转换:哈基米 == 猫 ? true :false
开发语言·前端·javascript·学习·面试·web
澄江静如练_6 小时前
列表渲染(v-for)
前端·javascript·vue.js
JustHappy7 小时前
「chrome extensions🛠️」我写了一个超级简单的浏览器插件Vue开发模板
前端·javascript·github
Loo国昌7 小时前
Vue 3 前端工程化:架构、核心原理与生产实践
前端·vue.js·架构
sg_knight7 小时前
拥抱未来:ECMAScript Modules (ESM) 深度解析
开发语言·前端·javascript·vue·ecmascript·web·esm
LYFlied7 小时前
【每日算法】LeetCode 17. 电话号码的字母组合
前端·算法·leetcode·面试·职场和发展