Skip to content

Hook 钩子大全

钩子让你在系统既有流程里插一脚,不用改核心代码。当前版本共 90 个点位,下面按类型分组列全。

怎么订阅

php
<?php
namespace App\Plugin\Demo\Hook;

use App\Controller\Base\View\UserPlugin;
use Kernel\Annotation\Hook;

class Main extends UserPlugin
{
    #[Hook(point: 0x130)]        // USER_VIEW_FOOTER
    public function footer(): void
    {
        echo '<script>console.log("hi")</script>';
    }
}

三条铁律

一、实参必须是变量。

hook() 的变参是按引用接收的,所以调用侧传字面量会直接致命 500:

php
hook(P, new X());            // 500
hook(P, $this->getUser());   // 500
hook(P, ['a' => 1]);         // 500
hook(P, '字面量');            // 500

而且只有真跑到那一行才炸。写核心埋点时先赋值给变量再传。

二、订阅方写十六进制字面量,不要引用常量。

php
#[Hook(point: 0x2300)]                        // 推荐
#[Hook(point: \App\Consts\Hook::XXX)]         // 老核心上会把插件卡死

注解参数在插件启用时求值。老版本核心没有这个常量会抛 Error,把插件卡在「START 已执行、STATUS 未写入」的半启用状态。

三、返回 bool 会短路整条链。

钩子方法返回 bool 时,后续订阅方不再执行,调用方直接拿到这个布尔值。目前只有 SERVICE_SMTP_SEND_BEFORE 用这个语义(返回 true = 邮件已由插件接管)。其余点位一律返回 void


视图型点位

这类点位没有参数,订阅方直接 echo 输出 HTML / CSS / JS。

后台

常量位置
ADMIN_VIEW_HEADER0x2后台全局头部,放 CSS
ADMIN_VIEW_FOOTER0x1后台全局底部,放 JS
ADMIN_VIEW_BODY0x10201后台全局 body
ADMIN_VIEW_MENU0x3后台左侧菜单,加自己的菜单项
ADMIN_VIEW_NAV0x4后台顶部导航
ADMIN_VIEW_AUTH_LOGIN_FORM0x60后台登录表单内
ADMIN_VIEW_USER_HEADER0x10002会员管理页头部
ADMIN_VIEW_USER_FOOTER0x9会员管理页底部
ADMIN_VIEW_USER_TOOLBAR0x10会员管理页按钮区
ADMIN_VIEW_COMMODITY_TOOLBAR0x7商品管理按钮区
ADMIN_VIEW_COMMODITY_FOOTER0x6商品管理底部
ADMIN_VIEW_CATEGORY_TOOLBAR0x701分类管理按钮区
ADMIN_VIEW_ORDER_TOOLBAR0x13订单管理按钮区
ADMIN_VIEW_ORDER_FOOTER0x12订单管理底部
ADMIN_VIEW_CARD_TOOLBAR0x801卡密管理按钮区
ADMIN_VIEW_CARD_FOOTER0x802卡密管理底部
ADMIN_VIEW_CONFIG_TOOLBAR0x14网站设置按钮区

菜单项示例:

php
#[Hook(point: 0x3)]
public function menu(): void
{
    echo '<div class="menu-item"><a class="menu-link" href="/plugin/Demo/api/index">'
       . '<span class="menu-title">我的插件</span></a></div>';
}

前台

常量位置
USER_VIEW_HEADER0x128前台头部
USER_VIEW_BODY0x129前台 body
USER_VIEW_FOOTER0x130前台底部
USER_GLOBAL_VIEW_HEADER0x228全局头部(含会员中心)
USER_GLOBAL_VIEW_BODY0x229全局 body
USER_GLOBAL_VIEW_FOOTER0x230全局底部
USER_VIEW_INDEX_HEADER0x10001首页头部
USER_VIEW_INDEX_BODY0x10003首页 body
USER_VIEW_INDEX_FOOTER0x10004首页底部
USER_VIEW_MENU0x57会员中心菜单
USER_VIEW_HEADER_NAV0x88商城顶栏导航(数组型,见下)
USER_VIEW_AUTH_LOGIN_BUTTON0x41登录按钮旁
USER_VIEW_AUTH_REGISTER_BUTTON0x42注册按钮旁
USER_VIEW_SECURITY_NAV0x43安全设置导航
USER_VIEW_PERSONAL_FORM0x44个人资料表单
USER_VIEW_QUERY_TRADE_NO0x89订单查询页

USER_VIEW_HEADER_NAV0x88)和别的视图点位不同:它是数组型的,插件返回一个导航条目,由各主题自己渲染,而不是直接 echo HTML。这样换主题也不会破版。

内核与后台表格

常量说明
KERNEL_INIT0x30内核初始化完成,最早的介入点。想拦截整个请求就用它
HACK_ROUTE_TABLE_COLUMNS0x2005给后台表格加列的唯一入口
HACK_ROUTE_TABLE_SEARCH0x2006给后台表格加搜索条件
HACK_SUBMIT_FORM0x9038给后台表单加字段
HACK_SUBMIT_TAB0x9039给后台表单加标签页
USER_API_AUTH_LOGIN_BEGIN0x21前台登录开始前
USER_API_AUTH_REGISTER_BEGIN0x19前台注册开始前

数据型点位

这类点位带参数,参数按引用传递,改了就会影响后续流程。

订单与支付

常量参数
USER_API_ORDER_TRADE_BEGIN0x16array $map 下单提交的原始数据
USER_API_ORDER_TRADE_PAY_BEGIN0x171Commodity $commodity, Order $order, Pay $pay
USER_API_ORDER_TRADE_AFTER0x17Commodity $commodity, Order $order, Pay $pay
USER_API_ORDER_PAY_AFTER0x18Commodity $commodity, Order $order, Pay $pay 付款完成
ORDER_MANUAL_DELIVERY_AFTER0x2200Order $order, bool $overwrite 手动发货写入之后
USER_API_RECHARGE_AFTER0x18191Recharge $recharge, Pay $pay 充值完成
SERVICE_PAY_CALLBACK_FAIL0x3010string $handle, string $reason, ?string $tradeNo, array $map

SERVICE_PAY_CALLBACK_FAIL$reason 取值:handlenot_foundcredentialpluginsignstatusduplicateamount。其中 sign/amount/handle/credential 通常意味着有人在伪造回调,duplicate 是网关重复通知(正常现象,别报警)。

ORDER_MANUAL_DELIVERY_AFTER 触发时 $order->secret 已是新内容、delivery_status = 1$overwrite 表示是不是覆盖了已有的发货内容。用来给买家补发「已发货」通知很合适。

账号

常量参数
USER_API_AUTH_REGISTER_AFTER0x20User $user
USER_API_AUTH_LOGIN_AFTER0x22User $user
USER_API_AUTH_LOGIN_FAIL0x23string $account, string $reason
ADMIN_API_AUTH_LOGIN_AFTER0x61Manage $manage
ADMIN_API_AUTH_LOGIN_FAIL0x62string $email, string $reason

前台失败 $reasonnot_foundpasswordbanned。 后台失败 $reasonthrottledcaptchanot_foundpasswordtotpbannedshiftother(等待两步验证码不算失败)。

拿来做登录爆破告警正好。

前台数据

这些点位可以改写返回给前台的数据,做隐藏商品、改价展示、加字段都靠它们。

常量参数
USER_API_INDEX_CATEGORY_LIST0x49array $category 分类列表
USER_API_INDEX_COMMODITY_LIST0x50array $data 商品列表
USER_API_INDEX_COMMODITY_DETAIL_INFO0x51array $item 商品详情
USER_API_INDEX_PAY_LIST0x53array $pay 可用支付方式
USER_API_INDEX_QUERY_LIST0x54array $data 订单查询结果
USER_API_INDEX_QUERY_SECRET0x55Order $order 查看卡密
USER_API_PURCHASE_RECORD_LIST0x56array $data 购买记录

商品与库存

常量参数
COMMODITY_CHANGE_AFTER0x8100int[] $ids, string $action, ?Commodity $before
CARD_CHANGE_AFTER0x8101int[] $commodityIds, string $reason
SERVICE_SHOP_GET_ITEM_STOCK0x8000Commodity $commodity, string $race, array $sku

COMMODITY_CHANGE_AFTER —— 商品新增、修改、删除、上下架、批量设置、对接同步之后触发,一律在数据库事务提交之后,拿到的一定是已落盘的变更。

$action 取值:createupdatedeletestatusbatchsync$before 只在单商品保存路径提供修改前的模型,其余为 null

批量路径(status/batch)给的是请求里的 id 集合,可能包含实际没变化的商品 —— 订阅方要自己和快照比对算差量,别假设每个 id 都真的变了。delete 触发时商品行已经没了,只能拿到 id。

CARD_CHANGE_AFTER —— 卡密池变化(也就是自动发货商品的库存变了)之后触发,同样在事务提交之后。

$commodityIds 是受影响的商品 id(不是卡密 id)。$reason 取值:importeditlockunlockselldelete

下单发货导致的库存下降不走这里,用 USER_API_ORDER_PAY_AFTERORDER_MANUAL_DELIVERY_AFTER

工单

常量参数
USER_API_TICKET_CREATE_AFTER0x2100Ticket $ticket, TicketMessage $message
USER_API_TICKET_REPLY_AFTER0x2101Ticket $ticket, TicketMessage $message
ADMIN_API_TICKET_REPLY_AFTER0x2102Ticket $ticket, TicketMessage $message, Manage $manage

三个都在事务提交之后触发,钩子内抛异常不影响接口结果。

邮件

常量参数
SERVICE_SMTP_SEND_BEFORE0x3000array $config, string $email, string $title, string $content
SERVICE_SMTP_SEND_SUCCESS0x3001同上
SERVICE_SMTP_SEND_ERROR0x3002同上

SERVICE_SMTP_SEND_BEFORE唯一用返回值短路的点位:返回 true 表示邮件已由插件接管,核心不再走 SMTP。想把邮件换成其他通道(Telegram、企业微信)就用它。

内核与路由

常量参数
CONTROLLER_CALL_BEFORE0x31object $controller, string $action
CONTROLLER_CALL_AFTER0object $controller, string $action, mixed $result
HTTP_ROUTE_RESPONSE0x47string $routePath, mixed $result
HTTP_NOT_FOUND0x48string $routePath 路由未命中
RENDER_VIEW0x33string $result 渲染结果,可改写 HTML
WAF_INTERCEPT0x289string $message WAF 拦截时
CSP_SOURCE_ALLOW0x8102array $sources CSP 放行源
LANG_MISS0x9100array $sourceList, array $langList 缺词条
ADMIN_API_PLUGIN_SAVE_CONFIG0x15int $id, array $map 插件配置保存时

HTTP_NOT_FOUND 拿来做扫描探测告警很好用 —— 短时间大量 404 基本就是有人在扫你。


风控 / 人工审核点位

0x2300 ~ 0x2305 是一组风控埋点。它们和别的钩子有两点不同,订阅前务必看完

一、全部按引用传 RiskContext $risk,订阅方改对象、返回 void

千万不要 return true/false —— 派发器遇到 bool 会短路整条链,第一个返回 bool 的订阅方会把后面所有风控插件一起挡掉。

php
public const PASS   = 0;   // 放行
public const LIMIT  = 1;   // 静默降权限额(订阅方自己挂软约束,核心不做特殊处理)
public const REVIEW = 2;   // 挂人工审核
public const DENY   = 3;   // 直接拒绝

$risk->escalate(RiskContext::DENY, '插件名', lang('理由'));  // 只升不降
$risk->hardAllow('插件名', '理由');                          // 强制放行并锁定
$risk->ref = 'AR-XXXX';                                      // 可查证编号,核心原样回显

核心在钩子返回后读 $risk->actionDENYJSONExceptionREVIEW 走各场景自己的挂起分支,LIMIT 核心不管。

二、为什么不能只靠抛异常

拒绝可以抛,但「挂人工审核」不行 —— 那需要核心知道「账号照建,但别给他签发会话」,光抛异常表达不了。

各点位

常量参数埋点位置
USER_API_AUTH_REGISTER_VALIDATED0x2300$risk, $user注册:校验后、落库前
USER_API_AUTH_PASSWORD_BEGIN0x2301$risk, $account找回密码:验证码校验之前
USER_API_RECHARGE_TRADE_BEGIN0x2302$risk, $user, $map充值下单:金额与通道校验之后
USER_API_CASH_SUBMIT_BEGIN0x2303$risk, $user, $map提现申请:绑定校验后、落库前
USER_API_TICKET_CREATE_BEGIN0x2304$risk, $user, $map工单创建:进 Service 之前
USER_API_ORDER_DELIVERY_BEGIN0x2305$risk, $order, $commodity发货之前

几个点位的设计说明:

注册(0x2300 —— 比 0x19 好在三点:用户名/邮箱/手机都是最终要入库的值且已去重;$user 按引用传,可以直接改字段;而且它在那个 try 之 —— try 会把任何异常改写成「注册失败」,放进去的话你给的理由到用户那儿就没了。REVIEW 时核心会把 $user->status 置 0 并跳过 loginSuccess(),否则用户会「注册成功」之后下一次点击就掉线。

找回密码(0x2301 —— 特意放在验证码校验之前:拒绝时不该白白消耗掉用户手里那条邮件/短信验证码,也不该替攻击者把站长的短信费烧掉。

充值(0x2302 —— 放服务层而不是控制器,因为控制器那边不组装 $map,金额是在服务层才解析出来的。$map只读上下文,改它没用(下游直接读 $_POST)。

提现(0x2303 —— REVIEW 不需要新状态:cash.status = 0 本来就是「待站长处理」,只有 type == 2(兑现到可消费余额)会自动到账,挂起时把这条捷径关掉即可。

发货之前(0x2305 —— 这是唯一能在卡密交出去之前把货扣下的位置,一处插入覆盖全部支付路径(0 元单、余额支付、各网关回调)。

钱已经收到了,此刻不该再谈「拒绝」,只该决定卡发不发 —— 所以订阅方只用 REVIEWdelivery_status 留 0、secret 换成提示文案,也就是手动发货商品在付款到发货之间的既有形态。被跳过的副作用(拉卡密、扣库存、分成与返利账单、发货邮件)一个都没执行过,所以审核通过后幂等地重跑一次恰好是对的。


已失效的点位

下面这 8 个常量还在 Hook.php 里,但核心已经没有任何地方调用它们了。订阅了不会报错,但永远不会被触发:

常量用什么替代
ADMIN_VIEW_USER_TABLE0x8HACK_ROUTE_TABLE_COLUMNS
ADMIN_VIEW_COMMODITY_TABLE0x5HACK_ROUTE_TABLE_COLUMNS
ADMIN_VIEW_CATEGORY_TABLE0x702HACK_ROUTE_TABLE_COLUMNS
ADMIN_VIEW_ORDER_TABLE0x11HACK_ROUTE_TABLE_COLUMNS
ADMIN_VIEW_CATEGORY_POST0x703HACK_SUBMIT_FORM
ADMIN_VIEW_COMMODITY_POST0x45HACK_SUBMIT_FORM
USER_VIEW_COMMODITY_POST0x46
USER_API_INDEX_TRADE_CALC_AMOUNT0x52

网上能搜到的老教程还在教用 ADMIN_VIEW_USER_TABLE 往后台表格加列(往里 echo 一段 JSON 列定义)。那套写法在当前版本已经完全无效。

给后台表格加列的正确姿势

现在唯一的入口是 HACK_ROUTE_TABLE_COLUMNS0x2005),配合 Column 实体使用,而不是 echo 一段 JSON。

两个容易踩的点:

  • 列渲染代码里 escapeHtml 不是全局可用的,要自己处理转义
  • Order.amount 是字符串,参与计算前先转换

基于 MIT 协议开源