本文详细介绍如何在 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 测试短信发送
-
输入测试手机号
-
点击"发送测试短信"
-
查看是否收到短信
核心原理
5.1 钩子工作机制
bash
用户触发短信发送
↓
系统调用 Sms::send()
↓
触发 sms_send 钩子
↓
框架查找钩子注册表
↓
调用 Juhesms::smsSend()
↓
调用 Service::sendCode()
↓
请求聚合数据 API
↓
返回发送结果
5.2 零侵入集成
插件通过钩子系统自动接管短信发送,**无需修改任何业务代码**:
php
// 原有业务代码(无需修改)
Sms::send($mobile, $code, 'register');
// 系统内部会自动调用插件的 smsSend() 方法
常见问题
6.1 短信发送失败
**可能原因**:
-
APPKEY 不正确
-
模板 ID 错误
-
模板未审核
-
余额不足
**解决方案**:
-
检查配置是否正确
-
查看 `runtime/log/` 日志文件
-
登录聚合数据后台查看状态
6.2 钩子未注册
**症状**:提示"请在后台插件管理安装短信验证插件"
**解决方案**:
- 检查 `addons.php` 配置
2.配置系统钩子和路由
-
后台重新启用插件
-
清除缓存
总结
7.1 技术要点
-
**插件架构**:遵循 FastAdmin 插件开发规范
-
**钩子系统**:利用钩子实现零侵入集成
-
**服务封装**:独立 Service 类封装 API 调用
-
**路由配置**:通过 addons.php 统一管理
7.2 开发建议
-
**代码规范**:遵循 PSR 标准,使用命名空间
-
**错误处理**:记录详细日志便于排查
-
**安全防护**:防轰炸、防刷机制必不可少
-
**文档完善**:提供详细的使用说明和示例