异次元发卡插件文档3.5.6

本知识库目录

异次元发卡 3.5.6 插件开发与部署指南

适用版本:异次元发卡 3.5.6(提交 60b26c56f23d02da94715b1fc92899f02c86f35f)。

本文只描述仓库中实际存在的插件机制。Demo 只能说明目录和注解写法,不能作为“自制插件可以直接启用”的完整示例。

1. 先看结论

1.1 插件如何被检测

程序通过 Kernel\Util\Plugin::getPlugins() 扫描:

TEXT1 行展开代码收起代码
app/Plugin/*/Config/Info.php

只要插件目录下存在可加载的 Config/Info.php,后台接口 /admin/api/plugin/getPlugins 就能读到插件元数据。检测插件不要求它出现在应用商店缓存中。

1.2 为什么原版不能正常使用自制插件

原版 kernel/Plugin/Hook.php 只在应用商店授权组件成功加载时读取:

TEXT1 行展开代码收起代码
runtime/plugin/hook

这个文件由商店插件授权链生成并加密。自制插件虽然能被 getPlugins() 扫描到,但它的 #[Hook] 不会因此自动注册;后台启停也会走商店插件的 _plugin_start() / _plugin_stop()。

1.3 当前仓库的本地插件改动

本仓库增加了两个有限、独立的本地插件路径:

文件作用
kernel/Plugin/Hook.php继续加载商店 Hook;另外扫描不在 runtime/plugin/store.cache 中、且 STATUS=1 的本地插件,并注册其 #[Hook]。
app/Controller/Admin/Api/Plugin.php对不在商店缓存中的插件直接写入 Config/Config.php,执行启停状态 Hook,并记录管理日志。

商店插件仍使用原来的加密缓存和授权流程。仓库根目录的 kernel/Plugin.php 是应用商店授权组件,不应修改、解密或伪造;本文不依赖修改它。

2. 插件目录

最小可用插件:

TEXT13 行展开代码收起代码
app/Plugin/OrderLog/
├─ Config/
│  ├─ Info.php       # 必须:名称、作者、版本、描述
│  ├─ Config.php     # 必须:运行配置,至少包含 STATUS
│  └─ Submit.php     # 可选:后台配置表单
├─ Hook/
│  └─ Main.php       # 可选:#[Hook] 和生命周期方法
├─ Controller/       # 可选:插件路由控制器
├─ View/             # 可选:控制器或 Hook 渲染的模板
├─ Support/          # 可选:业务辅助类
├─ Db/               # 可选:插件缓存文件,由 App\Util\Plugin 管理
├─ Wiki/             # 可选:README.md、Sidebar.md、Terms.md
└─ runtime.log       # 可选:运行日志,目录必须可写

2.1 命名规则

  • 目录名就是插件 ID,例如 OrderLog、TelegramNotification。

  • 目录名只能使用字母、数字和下划线,且必须以字母开头更稳妥。

  • 命名空间必须与目录名大小写一致:App\Plugin\OrderLog\...。

  • Hook 文件名必须与类名一致,例如 Hook/Main.php 对应 App\Plugin\OrderLog\Hook\Main。

  • Linux 区分大小写。不要照抄旧 CardAPI 示例中的大小写不一致写法。

  • 插件 ID 不要与 runtime/plugin/store.cache 中的商店插件重名;命中该缓存后会按商店插件处理,不会走本地路径。

3. 必需配置文件

3.1 Config/Info.php

php12 行展开代码收起代码
<?php
declare(strict_types=1);

use App\Consts\Plugin;

return [
    Plugin::NAME => '订单日志',
    Plugin::AUTHOR => '你的名称',
    Plugin::WEB_SITE => '#',
    Plugin::DESCRIPTION => '记录订单付款成功事件。',
    Plugin::VERSION => '1.0.0',
];

Plugin::NAME 是后台显示名称,目录名仍是插件 ID。Info.php 返回值不是对象,必须是数组。

3.2 Config/Config.php

php6 行展开代码收起代码
<?php
declare(strict_types=1);

return [
    'STATUS' => '0',
];

配置会被后台 setConfig 重新写成 PHP 数组。不要手工把 Token、数据库密码等秘密提交到 Git。

3.3 Config/Submit.php

这是后台“配置”弹窗的字段定义。每个字段至少有 title、name、type;name 必须与 Config.php 的键一致。

常用类型:

type用途常用键
input单行文本placeholder、required、default
password密钥输入同 input
number数字输入required、default
textarea多行文本height、default
select下拉选择dict: [[id, name], ...]
switch开关,提交通常是 0/1placeholder、default
checkbox多选dict、default
jsonJSON 编辑框default
explain只显示说明,不保存业务值placeholder

dict 可以直接写数组,也可以写数据字典字符串,例如 API 通知插件使用的 commodity,id,name。具体字典由当前版本的后台组件解析,插件不应假设未知字典一定存在。

示例:

php12 行展开代码收起代码
<?php
declare(strict_types=1);

return [
    [
        'title' => '记录日志',
        'name' => 'log',
        'type' => 'switch',
        'default' => '1',
        'placeholder' => '记录|关闭',
    ],
];

Kernel\Util\Plugin::getPlugin() 会把 Config.php 中已有的值填回 PLUGIN_SUBMIT[*].default;不存在的值才使用 Submit.php 给出的默认值。

4. Hook 机制

4.1 基本写法

php20 行展开代码收起代码
<?php
declare(strict_types=1);

namespace App\Plugin\OrderLog\Hook;

use App\Consts\Hook as HookPoint;
use App\Model\Commodity;
use App\Model\Order;
use App\Model\Pay;
use App\Util\Plugin;
use Kernel\Annotation\Hook;

final class Main
{
    #[Hook(point: HookPoint::USER_API_ORDER_PAY_AFTER)]
    public function orderPaid(Commodity $commodity, Order $order, ?Pay $pay): void
    {
        Plugin::log('OrderLog', "订单 {$order->trade_no} 已付款");
    }
}

要求:

  1. Hook 方法必须是 public。

  2. #[Hook] 必须使用命名参数 point:,值来自 App\Consts\Hook。

  3. 方法参数顺序必须和挂载点实际传参一致;类型声明错误会让订单流程报错。

  4. Hook 方法应尽快返回。同步网络请求会增加付款请求耗时,失败应捕获异常,不能阻止支付和发货。

  5. 本地插件只有 STATUS=1 才注册;停用后下一次请求不会再调用它。

4.2 Hook 返回值

底层 Kernel\Util\Plugin::hook() 的行为:

  • string:追加到当前结果,适合输出 HTML/JS。

  • array:追加到结果数组,适合表格列、导航等结构化扩展。

  • Kernel\Plugin\Entity\Stock:立即返回库存对象。

  • null/无返回值:不改变结果。

页面挂载点常见做法是直接 echo,数据型挂载点则应按注释要求修改引用参数或返回数组。

4.3 3.5.6 挂载点速查

完整常量以 app/Consts/Hook.php 为准。常用挂载点如下:

常量触发位置 / 参数
ADMIN_VIEW_MENU后台左侧菜单,通常直接输出 HTML
ADMIN_VIEW_HEADER / ADMIN_VIEW_FOOTER后台 CSS / JS
ADMIN_VIEW_NAV后台应用商店旁导航
ADMIN_VIEW_USER_TABLE ADMIN_VIEW_USER_TOOLBAR ADMIN_VIEW_USER_FOOTER后台会员页
ADMIN_VIEW_COMMODITY_TABLE ADMIN_VIEW_COMMODITY_TOOLBAR ADMIN_VIEW_COMMODITY_FOOTER后台商品页
ADMIN_VIEW_COMMODITY_POST商品添加/编辑表单扩展
ADMIN_VIEW_CATEGORY_TOOLBAR ADMIN_VIEW_CATEGORY_TABLE ADMIN_VIEW_CATEGORY_POST后台分类页
ADMIN_VIEW_ORDER_TABLE ADMIN_VIEW_ORDER_TOOLBAR ADMIN_VIEW_ORDER_FOOTER后台订单页
ADMIN_VIEW_CONFIG_TOOLBAR网站设置页工具栏,返回二维数组
ADMIN_API_PLUGIN_SAVE_CONFIG保存插件配置,参数为 string $pluginName, array $postMap
USER_API_ORDER_TRADE_BEGIN下单前,参数为 array $_POST
USER_API_ORDER_TRADE_AFTER下单成功,Commodity $commodity, Order $order, Pay $pay
USER_API_ORDER_TRADE_PAY_BEGIN下单后、发起支付前,订单相关对象
USER_API_ORDER_PAY_AFTER付款成功,Commodity $commodity, Order $order, Pay $pay
USER_API_RECHARGE_AFTER充值付款成功,UserRecharge $recharge, Pay $pay
USER_API_AUTH_REGISTER_BEGIN/AFTER注册前 / 注册后
USER_API_AUTH_LOGIN_BEGIN/AFTER登录前 / 登录后
KERNEL_INIT核心初始化完成
CONTROLLER_CALL_BEFORE/AFTER控制器调用前 / 后
RENDER_VIEW视图渲染,传入视图原文引用
USER_VIEW_AUTH_LOGIN_BUTTON / USER_VIEW_AUTH_REGISTER_BUTTON前台登录 / 注册按钮
USER_VIEW_SECURITY_NAV / USER_VIEW_PERSONAL_FORM用户安全中心 / 个人资料
USER_VIEW_COMMODITY_POST前台商品表单
HTTP_ROUTE_RESPONSEHTTP 路由响应返回用户前
USER_VIEW_INDEX_HEADER/BODY/FOOTER前台首页三个区域
USER_API_INDEX_CATEGORY_LIST分类列表,数组引用
USER_API_INDEX_COMMODITY_LIST商品列表,数组引用
USER_API_INDEX_COMMODITY_DETAIL_INFO商品详情数组引用
USER_API_INDEX_TRADE_CALC_AMOUNT下单金额计算结果数组引用
USER_API_INDEX_PAY_LIST支付列表数组引用
USER_API_INDEX_QUERY_LIST查询订单列表数组引用
USER_API_INDEX_QUERY_SECRET查询卡密,Order $order
USER_API_PURCHASE_RECORD_LIST购买记录列表数组引用
USER_VIEW_MENU / USER_VIEW_HEADER_NAV前台菜单 / 顶部导航
USER_VIEW_QUERY_TRADE_NO订单号后扩展内容
USER_VIEW_HEADER/BODY/FOOTER前台公共布局区域
USER_GLOBAL_VIEW_HEADER/BODY/FOOTER前台全局区域
WAF_INTERCEPTWAF 拦截,参数为 $message
SERVICE_SMTP_SEND_BEFORE/SUCCESS/ERROR邮件发送前 成功 失败
HACK_ROUTE_TABLE_COLUMNS订单表格列扩展
HACK_ROUTE_TABLE_SEARCH订单表格搜索扩展
HACK_SUBMIT_FORM / HACK_SUBMIT_TAB表单 / 标签页扩展
SERVICE_SHOP_GET_ITEM_STOCK商品库存扩展,返回 Stock

不要凭常量名称猜参数,开发前应打开 app/Consts/Hook.php 和触发点调用处搜索 hook(,以当前源码为准。

5. 生命周期

生命周期注解使用 Kernel\Annotation\Plugin,建议始终写命名参数:

php7 行展开代码收起代码
use Kernel\Annotation\Plugin as PluginState;

#[PluginState(state: PluginState::START)]
public function start(): void
{
    // 启用时执行
}

可用状态:

状态常量说明
启动Plugin::START后台启用插件时执行
停止Plugin::STOP后台停用插件时执行
卸载Plugin::UNINSTALL卸载流程使用
安装Plugin::INSTALL安装流程使用
升级Plugin::UPGRADE升级流程使用
保存配置Plugin::SAVE_CONFIG保存配置时执行,即使插件未启用

当前本地插件分支在启用/停用时直接调用 runHookState()。手工复制目录不会自动执行“安装”或“升级”迁移;如果插件需要建表,必须在正式安装流程中调用,或提供一次性、可重复执行的管理操作。

6. 控制器、视图和 URL

插件控制器通过 /plugin/{PluginID}/{Controller}/{method} 访问。例如:

TEXT1 行展开代码收起代码
/plugin/OrderLog/api/ping

对应:

TEXT2 行展开代码收起代码
app/Plugin/OrderLog/Controller/Api.php
App\Plugin\OrderLog\Controller\Api::ping()

后台控制器继承 App\Controller\Base\API\ManagePlugin 或 App\Controller\Base\View\ManagePlugin;前台控制器使用对应的 UserPlugin。后台接口应加 ManageSession,涉及输入时加 Waf,例如:

php18 行展开代码收起代码
<?php
declare(strict_types=1);

namespace App\Plugin\OrderLog\Controller;

use App\Controller\Base\API\ManagePlugin;
use App\Interceptor\ManageSession;
use App\Interceptor\Waf;
use Kernel\Annotation\Interceptor;

#[Interceptor([Waf::class, ManageSession::class], Interceptor::TYPE_API)]
final class Api extends ManagePlugin
{
    public function ping(): array
    {
        return $this->json(200, 'ok');
    }
}

视图由基类的 render() 从插件自己的 View/ 目录读取。Hook 中常见写法:

php1 行展开代码收起代码
echo $this->render(null, 'Menu.html');

路由访问插件控制器前必须启用插件;Hook::load() 会对未启用插件重定向到首页。

7. 日志、缓存和数据库

7.1 运行日志

php4 行展开代码收起代码
use App\Util\Plugin;

Plugin::log('OrderLog', '处理完成');
$config = Plugin::getConfig('OrderLog');

日志文件是:

TEXT1 行展开代码收起代码
app/Plugin/OrderLog/runtime.log

后台插件列表中的“日志”按钮读取该文件。不要写入 Bot Token、密码、卡密或完整用户敏感信息。

7.2 插件缓存

App\Util\Plugin 已提供 setCache()、getCache()、getCaches()、delCache() 和 clearCache(),数据保存到:

TEXT1 行展开代码收起代码
app/Plugin/{PluginID}/Db/{name}.php

这不是高并发数据库。需要查询、事务或持久业务数据时使用现有 Eloquent 模型和数据库迁移逻辑。

8. 安装、启用和打包

8.1 手工安装

把插件目录放到:

TEXT1 行展开代码收起代码
app/Plugin/{PluginID}

然后:

  1. 检查 Config/Info.php、Config/Config.php 和命名空间。

  2. 确认目录名不是 store.cache 中已有插件 ID。

  3. 后台进入“通用插件”。

  4. 清空状态筛选,找到插件并打开配置。

  5. 保存配置后点击“启用”。不要只手工把 STATUS 改成 1,因为启用动作还负责注册 Hook。

8.2 ZIP 结构

压缩包根目录必须直接包含:

TEXT4 行展开代码收起代码
Config/
Hook/
Support/
Wiki/

错误结构:

TEXT1 行展开代码收起代码
TelegramNotification/Config/Info.php

如果安装器把这个 ZIP 解压到 app/Plugin/,最终会多一层目录,插件就不会被检测到。

8.3 1Panel / Linux 权限

PHP-FPM 用户必须能遍历插件目录并读写配置、日志。推荐:

Bash2 行展开代码收起代码
find app/Plugin/{PluginID} -type d -exec chmod 755 {} \;
find app/Plugin/{PluginID} -type f -exec chmod 644 {} \;

如果后台能看到插件但保存配置失败、日志为空,优先检查权限。Windows 生成的 ZIP 可能没有 Unix 目录模式,上传后目录会变成 644,PHP-FPM 无法进入 Config/、Hook/ 或 Support/。

8.4 修改代码后的缓存

本地 Hook 当前按请求扫描,通常刷新页面即可看到新代码;但 PHP OPcache 可能继续使用旧字节码。修改 Hook、控制器或模板后,按顺序处理:

  1. 后台停用再启用插件。

  2. 清理 PHP OPcache,或重启对应 PHP-FPM 容器。

  3. 浏览器强制刷新后台页面。

9. 最小自制插件示例

目录:

TEXT4 行展开代码收起代码
app/Plugin/OrderLog/
├─ Config/Info.php
├─ Config/Config.php
└─ Hook/Main.php

Config/Info.php:

php12 行展开代码收起代码
<?php
declare(strict_types=1);

use App\Consts\Plugin;

return [
    Plugin::NAME => '订单日志',
    Plugin::AUTHOR => '本地',
    Plugin::WEB_SITE => '#',
    Plugin::DESCRIPTION => '付款成功后写入插件日志。',
    Plugin::VERSION => '1.0.0',
];

Config/Config.php:

php4 行展开代码收起代码
<?php
declare(strict_types=1);

return ['STATUS' => '0'];

Hook/Main.php:

php20 行展开代码收起代码
<?php
declare(strict_types=1);

namespace App\Plugin\OrderLog\Hook;

use App\Consts\Hook as HookPoint;
use App\Model\Commodity;
use App\Model\Order;
use App\Model\Pay;
use App\Util\Plugin;
use Kernel\Annotation\Hook;

final class Main
{
    #[Hook(point: HookPoint::USER_API_ORDER_PAY_AFTER)]
    public function orderPaid(Commodity $commodity, Order $order, ?Pay $pay): void
    {
        Plugin::log('OrderLog', "{$commodity->name} / {$order->trade_no}");
    }
}

自检:

Bash2 行展开代码收起代码
php -l app/Plugin/OrderLog/Config/Info.php
php -l app/Plugin/OrderLog/Hook/Main.php

启用后创建测试订单,再在插件“日志”中确认是否出现订单号。

10. Telegram 订单通知插件

仓库中的 app/Plugin/TelegramNotification 是完整的付款成功通知实现,适合直接使用或作为网络通知插件参考。 位于 https://blog.mhdy.net/archives/yi-ci-yuan-fa-qia-de-zi-zhi-cha-jian

配置步骤

  1. 在 @BotFather 使用 /newbot 创建 Bot,保存 Token。

  2. 让 Bot 在目标私聊、群组或频道中收到一条消息。

  3. 调用 https://api.telegram.org/bot<TOKEN>/getUpdates 获取 message.chat.id。

  4. 论坛群需要额外填写 message_thread_id;普通群留空。

  5. 后台填写 Token、Chat ID,保存后点击启用。

重要配置

  • 默认只发送纯文本,不发送卡密。

  • commodity_mode=1 时只通知选中的商品。

  • notify_free=0 可忽略零元/赠送订单。

  • 联系方式默认脱敏;完整显示前确认隐私和群权限。

  • timeout 建议 3 秒以内;网络失败只记录日志,不阻塞支付和发货。

  • 自定义 API 只允许填写自己控制的 HTTPS 代理;Token 和订单内容会发送到该地址。

完整变量和故障排查见 app/Plugin/TelegramNotification/Wiki/README.md。

11. 自检与故障排查

11.1 插件不显示

依次检查:

TEXT4 行展开代码收起代码
app/Plugin/{PluginID}/Config/Info.php        存在且返回数组
app/Plugin/{PluginID}/Config/Info.php        PHP 语法正确
插件目录名与命名空间                         大小写一致
后台状态筛选                                 已清空或选择“未运行”

查看后台接口 /admin/api/plugin/getPlugins 的响应,确认列表中有对应 id。

11.2 能显示但 Hook 不执行

  • Config/Config.php 的 STATUS 是否为 1。

  • 是否点击了后台“启用”,而不是只改文件。

  • Hook 类名、文件名、命名空间是否一致。

  • 方法是否 public,是否使用 #[Hook(point: ...)]。

  • 插件 ID 是否误出现在 runtime/plugin/store.cache。

  • PHP-FPM 用户是否能读取 Hook/ 目录。

  • 是否需要清理 OPcache。

可在测试环境运行已有检查脚本:

Bash1 行展开代码收起代码
php tests/local_plugin_hook.php

该脚本会临时启用 Telegram 插件,确认 USER_API_ORDER_PAY_AFTER 已注册,然后恢复原配置。

11.3 配置或日志写入失败

检查:

Bash3 行展开代码收起代码
ls -ld app/Plugin/{PluginID} app/Plugin/{PluginID}/Config
ls -l app/Plugin/{PluginID}/Config/Config.php
ls -l app/Plugin/{PluginID}/runtime.log

目录建议 755,文件建议 644;运行用户需要对配置文件和日志文件具有写权限。不要为了省事把整个站点改成 777。

11.4 订单流程被插件拖慢或报错

  • 网络请求设置短超时,并捕获 Throwable。

  • 不要在付款 Hook 中修改订单状态,除非插件明确负责发货且已处理幂等。

  • 不要把卡密放进外部通知。

  • 日志只记录订单号、结果和脱敏错误。

  • 先停用插件验证问题是否消失,再查看 runtime.log。

12. 发布前检查清单

  • 插件 ID 唯一,目录和命名空间大小写一致。

  • Config/Info.php 返回完整元数据。

  • Config/Config.php 至少有 STATUS。

  • 所有外部输入经过验证,后台接口有会话和 WAF 保护。

  • Hook 参数与触发点实际调用一致。

  • 网络调用有超时、异常处理和日志脱敏。

  • 不发送卡密、Token、密码和不必要的个人信息。

  • ZIP 根目录没有多余的插件名外层目录。

  • Linux 目录 755、文件 644,PHP-FPM 可读写必要文件。

  • 在测试订单上验证启用、停用、重复触发和网络失败场景。

0%