FastAdmin 短信插件开发完整教程

本文详细介绍如何在 FastAdmin 框架下开发一个完整的短信插件,基于聚合数据 API 实现验证码和通知短信发送功能。文章涵盖插件架构设计、钩子系统、路由配置等核心内容,适合 FastAdmin 插件开发初学者参考。

目录

项目背景

插件架构设计

[2.1 目录结构](#2.1 目录结构)

[2.2 核心组件说明](#2.2 核心组件说明)

开发步骤

[3.1 创建插件元数据](#3.1 创建插件元数据)

[3.2 配置插件参数](#3.2 配置插件参数)

[3.3 开发插件主类](#3.3 开发插件主类)

[3.4 封装短信服务类](#3.4 封装短信服务类)

[3.5 开发前台控制器](#3.5 开发前台控制器)

[3.6 配置系统钩子和路由](#3.6 配置系统钩子和路由)

测试验证

[4.1 访问插件首页](#4.1 访问插件首页)

[4.2 测试短信发送](#4.2 测试短信发送)

核心原理

[5.1 钩子工作机制](#5.1 钩子工作机制)

[5.2 零侵入集成](#5.2 零侵入集成)

常见问题

[6.1 短信发送失败](#6.1 短信发送失败)

[6.2 钩子未注册](#6.2 钩子未注册)

总结

[7.1 技术要点](#7.1 技术要点)

[7.2 开发建议](#7.2 开发建议)


项目背景

在现代 Web 应用中,短信验证是用户注册、登录、找回密码等场景的必备功能。FastAdmin 作为一款流行的后台开发框架,提供了完善的短信钩子系统,允许开发者通过插件方式扩展短信功能。

本教程将带你从零开始,开发一个功能完整的短信插件,支持:

  • ✅ 验证码短信(注册/登录/重置密码/更换手机)

  • ✅ 通知短信

  • ✅ 防轰炸机制

  • ✅ 前台测试页面

  • ✅ 诊断修复工具

  • ✅ 详情文档页面

插件架构设计

2.1 目录结构

bash 复制代码
addons/juhesms/
├── Juhesms.php              # 插件主类
├── config.php               # 配置信息
├── info.ini                 # 插件元数据
├── controller/
│   └── Index.php            # 前台控制器
├── library/
│   └── Service.php          # 短信服务类
└── view/
    └── index/
        ├── index.html       # 前台首页
        └── detail.html      # 详情页面

2.2 核心组件说明

bash 复制代码
| 文件 | 作用 |
|------|------|
| `Juhesms.php` | 插件主类,包含钩子监听器 |
| `config.php` | 定义插件配置项 |
| `info.ini` | 插件元数据 |
| `Index.php` | 处理前台请求 |
| `Service.php` | 封装 API 调用 |

开发步骤

3.1 创建插件元数据

文件:`addons/juhesms/info.ini`

bash 复制代码
name = juhesms
title = 聚合数据短信
intro = 使用聚合数据短信接口发送验证码和通知短信
author = Yujl
website = https://www.juhe.cn/
version = 1.0.0
state = 1
url = /addons/juhesms

3.2 配置插件参数

文件:`addons/juhesms/config.php`

php 复制代码
<?php

return [
    [
        'name' => 'appkey',
        'title' => 'APPKEY',
        'type' => 'string',
        'value' => '',
        'rule' => 'required',
        'tip' => '在聚合数据个人中心查看',
    ],
    [
        'name' => 'tpl_register',
        'title' => '注册验证码模板 ID',
        'type' => 'string',
        'value' => '',
        'rule' => 'required',
        'tip' => '在聚合数据个人中心查看',
    ],
    [
        'name' => 'tpl_login',
        'title' => '登录验证码模板 ID',
        'type' => 'string',
        'value' => '',
        'rule' => 'required',
        'tip' => '在聚合数据个人中心查看',
    ],
    [
        'name' => 'tpl_resetpwd',
        'title' => '重置密码模板 ID',
        'type' => 'string',
        'value' => '',
        'rule' => 'required',
        'tip' => '在聚合数据个人中心查看',
    ],
    [
        'name' => 'tpl_changemobile',
        'title' => '更换手机模板 ID',
        'type' => 'string',
        'value' => '',
        'rule' => 'required',
        'tip' => '在聚合数据个人中心查看',
    ],
];

3.3 开发插件主类

文件:`addons/juhesms/Juhesms.php`

php 复制代码
<?php

namespace addons\juhesms;

use addons\juhesms\library\Service as SmsService;
use think\Addons;

/**
 * 聚合数据短信插件
 */
class Juhesms extends Addons
{
    /**
     * 插件安装
     */
    public function install()
    {
        $this->clearCache();
        return true;
    }

    /**
     * 插件卸载
     */
    public function uninstall()
    {
        $this->clearCache();
        return true;
    }
    
    /**
     * 清除缓存
     */
    protected static function clearCache()
    {
        $cacheFiles = [
            RUNTIME_PATH . 'config.php',
            RUNTIME_PATH . 'tags.php',
        ];
        
        foreach ($cacheFiles as $file) {
            if (file_exists($file)) {
                @unlink($file);
            }
        }
        
        if (function_exists('opcache_reset')) {
            @opcache_reset();
        }
    }

    /**
     * 短信发送(核心钩子)
     * @param \app\common\model\Sms $sms
     * @return bool
     */
    public function smsSend($sms)
    {
        if (!$sms || !$sms->mobile) {
            return false;
        }

        $code = $sms->code;
        $mobile = $sms->mobile;
        $event = $sms->event;

        // 调用服务类发送短信
        $result = SmsService::sendCode($mobile, $code, $event);
        
        return $result;
    }

    /**
     * 短信通知
     * @param array $params
     * @return bool
     */
    public function smsNotice(&$params)
    {
        $mobile = $params['mobile'] ?? '';
        $msg = $params['msg'] ?? '';

        if (!$mobile) {
            return false;
        }

        return SmsService::sendNotice($mobile, $msg);
    }

    /**
     * 获取验证码事件
     */
    public function smsGet($sms)
    {
        // 可选:记录日志
    }

    /**
     * 校验验证码事件
     */
    public function smsCheck($sms)
    {
        return true;
    }

    /**
     * 清空验证码事件
     */
    public function smsFlush()
    {
        // 可选:记录日志
    }
}

3.4 封装短信服务类

文件:`addons/juhesms/library/Service.php`

php 复制代码
<?php

namespace addons\juhesms\library;

use fast\Http;

/**
 * 聚合数据短信服务类
 */
class Service
{
    /**
     * API 接口地址
     */
    const API_URL = 'http://v.juhe.cn/sms/send';

    /**
     * 发送短信
     * @param string $mobile 手机号
     * @param array $vars 模板变量
     * @param int $tplId 模板 ID
     * @param string $appKey APPKEY
     * @return bool
     */
    public static function send($mobile, $vars = [], $tplId = null, $appKey = null)
    {
        if (!$appKey) {
            $config = get_addon_config('juhesms');
            $appKey = $config['appkey'] ?? '';
        }

        if (!$tplId) {
            return false;
        }

        $params = [
            'key'    => $appKey,
            'mobile' => $mobile,
            'tpl_id' => $tplId,
        ];

        if (!empty($vars)) {
            $params['vars'] = json_encode($vars, JSON_UNESCAPED_UNICODE);
        }

        try {
            $result = self::request($params);
            if ($result && $result['error_code'] == 0) {
                return true;
            } else {
                \think\Log::record('聚合短信发送失败:' . json_encode($result));
                return false;
            }
        } catch (\Exception $e) {
            \think\Log::record('聚合短信发送异常:' . $e->getMessage());
            return false;
        }
    }

    /**
     * 发送验证码短信
     * @param string $mobile 手机号
     * @param string $code 验证码
     * @param string $event 事件类型
     * @return bool
     */
    public static function sendCode($mobile, $code, $event)
    {
        $config = get_addon_config('juhesms');
        
        // 根据事件获取模板 ID
        $tplMap = [
            'register'     => 'tpl_register',
            'login'        => 'tpl_login',
            'resetpwd'     => 'tpl_resetpwd',
            'changemobile' => 'tpl_changemobile',
        ];

        $tplId = $config[$tplMap[$event] ?? ''] ?? null;

        return self::send($mobile, ['code' => $code], $tplId);
    }

    /**
     * 发送通知短信
     * @param string $mobile 手机号
     * @param string $msg 消息内容
     * @return bool
     */
    public static function sendNotice($mobile, $msg)
    {
        $config = get_addon_config('juhesms');
        $tplId = $config['tpl_notice'] ?? null;

        return self::send($mobile, ['msg' => $msg], $tplId);
    }

    /**
     * 发送 HTTP 请求
     */
    protected static function request($params)
    {
        $result = Http::sendRequest(self::API_URL, $params);
        
        if ($result['ret']) {
            return json_decode($result['msg'], true);
        }
        
        return false;
    }
}

3.5 开发前台控制器

**文件**:`addons/juhesms/controller/Index.php`

php 复制代码
<?php

namespace addons\juhesms\controller;

use think\addons\Controller;

/**
 * 前台控制器
 */
class Index extends Controller
{
    /**
     * 插件首页
     */
    public function index()
    {
        $config = get_addon_config('juhesms');
        $this->view->assign('config', $config);
        return $this->view->fetch();
    }

    /**
     * 详情页(渲染 README)
     */
    public function detail()
    {
        $readmeFile = __DIR__ . '/../README.md';
        
        if (file_exists($readmeFile)) {
            $content = file_get_contents($readmeFile);
            $content = $this->parseMarkdown($content);
        } else {
            $content = '<p>暂无详细说明</p>';
        }
        
        $this->view->assign('content', $content);
        return $this->view->fetch();
    }

    /**
     * 测试短信发送
     */
    public function test()
    {
        if (!$this->request->isPost()) {
            return json(['code' => 0, 'msg' => '请求方法错误']);
        }

        if (!\app\admin\library\Auth::instance()->id) {
            return json(['code' => 0, 'msg' => '请先登录']);
        }

        $mobile = $this->request->post('mobile', '', 'trim');
        
        if (!$mobile || !preg_match('/^1\d{10}$/', $mobile)) {
            return json(['code' => 0, 'msg' => '请输入正确的手机号']);
        }

        $code = \fast\Random::numeric(4);
        $result = \app\common\library\Sms::send($mobile, $code, 'login');
        
        if ($result) {
            return json(['code' => 1, 'msg' => '发送成功,验证码:' . $code]);
        } else {
            return json(['code' => 0, 'msg' => '发送失败']);
        }
    }

    /**
     * 诊断工具
     */
    public function diagnose()
    {
        $result = ['checks' => [], 'success' => 0, 'failed' => 0];

        // 清除缓存
        $cacheFiles = [RUNTIME_PATH . 'config.php', RUNTIME_PATH . 'tags.php'];
        foreach ($cacheFiles as $file) {
            if (file_exists($file) && @unlink($file)) {
                $result['checks'][] = ['item' => '清除缓存', 'status' => 'success'];
                $result['success']++;
            }
        }

        // 检查插件文件
        $requiredFiles = ['info.ini', 'Juhesms.php', 'config.php'];
        foreach ($requiredFiles as $file) {
            if (file_exists(__DIR__ . '/../' . $file)) {
                $result['checks'][] = ['item' => '文件检查', 'status' => 'success'];
                $result['success']++;
            } else {
                $result['checks'][] = ['item' => '文件检查', 'status' => 'failed'];
                $result['failed']++;
            }
        }

        return json([
            'code' => 1,
            'msg' => '诊断完成',
            'data' => $result
        ]);
    }

    /**
     * Markdown 解析
     */
    protected function parseMarkdown($markdown)
    {
        $markdown = preg_replace('/^### (.*+)$/m', '<h3>$1</h3>', $markdown);
        $markdown = preg_replace('/^## (.*+)$/m', '<h2>$1</h2>', $markdown);
        $markdown = preg_replace('/^# (.*+)$/m', '<h1>$1</h1>', $markdown);
        $markdown = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $markdown);
        $markdown = preg_replace('/`([^`]+)`/', '<code>$1</code>', $markdown);
        return $markdown;
    }
}

3.6 配置系统钩子和路由

文件:`application/extra/addons.php`

php 复制代码
<?php

return [
    'autoload' => false,
    
    // 钩子配置
    'hooks' => [
        'sms_send' => [
            'juhesms',
        ],
        'sms_notice' => [
            'juhesms',
        ],
        'sms_get' => [
            'juhesms',
        ],
        'sms_check' => [
            'juhesms',
        ],
        'sms_flush' => [
            'juhesms',
        ],
    ],
];

测试验证

4.1 访问插件首页

bash 复制代码
http://你的域名/juhesms

4.2 测试短信发送

  1. 输入测试手机号

  2. 点击"发送测试短信"

  3. 查看是否收到短信

核心原理

5.1 钩子工作机制

bash 复制代码
用户触发短信发送
    ↓
系统调用 Sms::send()
    ↓
触发 sms_send 钩子
    ↓
框架查找钩子注册表
    ↓
调用 Juhesms::smsSend()
    ↓
调用 Service::sendCode()
    ↓
请求聚合数据 API
    ↓
返回发送结果

5.2 零侵入集成

插件通过钩子系统自动接管短信发送,**无需修改任何业务代码**

php 复制代码
// 原有业务代码(无需修改)
Sms::send($mobile, $code, 'register');

// 系统内部会自动调用插件的 smsSend() 方法

常见问题

6.1 短信发送失败

**可能原因**

  1. APPKEY 不正确

  2. 模板 ID 错误

  3. 模板未审核

  4. 余额不足

**解决方案**

  • 检查配置是否正确

  • 查看 `runtime/log/` 日志文件

  • 登录聚合数据后台查看状态

6.2 钩子未注册

**症状**:提示"请在后台插件管理安装短信验证插件"

**解决方案**

  1. 检查 `addons.php` 配置

2.配置系统钩子和路由

  1. 后台重新启用插件

  2. 清除缓存

总结

7.1 技术要点

  1. **插件架构**:遵循 FastAdmin 插件开发规范

  2. **钩子系统**:利用钩子实现零侵入集成

  3. **服务封装**:独立 Service 类封装 API 调用

  4. **路由配置**:通过 addons.php 统一管理

7.2 开发建议

  1. **代码规范**:遵循 PSR 标准,使用命名空间

  2. **错误处理**:记录详细日志便于排查

  3. **安全防护**:防轰炸、防刷机制必不可少

  4. **文档完善**:提供详细的使用说明和示例

相关推荐
BingoGo2 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack2 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
用户3074596982073 天前
PHP 扩展——从入门到理解
php
鹏仔先生4 天前
拷贝漫画APP下载页PHP程序,后台带免费AI写作
php
云水一下4 天前
从零开始学 PHP 系列(一):PHP 的前世今生与开发环境搭建
开发语言·php
xingpanvip4 天前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
酉鬼女又兒4 天前
零基础入门计算机网络运输层:端到端通信核心作用、端口号分类规则、复用分用工作机制及UDP与TCP协议全方位对比详解
网络·网络协议·tcp/ip·计算机网络·考研·udp·php
dog2504 天前
不要再继续优化 TCP
网络协议·tcp/ip·php
Channing Lewis4 天前
PHP 解析 Excel 的那些坑:一次“行号错位”引发的数据丢失
开发语言·php·excel
Cheng小攸4 天前
渗透行为分析与检测
开发语言·php