Hook 钩子大全
钩子让你在系统既有流程里插一脚,不用改核心代码。当前版本共 90 个点位,下面按类型分组列全。
怎么订阅
<?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:
hook(P, new X()); // 500
hook(P, $this->getUser()); // 500
hook(P, ['a' => 1]); // 500
hook(P, '字面量'); // 500而且只有真跑到那一行才炸。写核心埋点时先赋值给变量再传。
二、订阅方写十六进制字面量,不要引用常量。
#[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_HEADER | 0x2 | 后台全局头部,放 CSS |
ADMIN_VIEW_FOOTER | 0x1 | 后台全局底部,放 JS |
ADMIN_VIEW_BODY | 0x10201 | 后台全局 body |
ADMIN_VIEW_MENU | 0x3 | 后台左侧菜单,加自己的菜单项 |
ADMIN_VIEW_NAV | 0x4 | 后台顶部导航 |
ADMIN_VIEW_AUTH_LOGIN_FORM | 0x60 | 后台登录表单内 |
ADMIN_VIEW_USER_HEADER | 0x10002 | 会员管理页头部 |
ADMIN_VIEW_USER_FOOTER | 0x9 | 会员管理页底部 |
ADMIN_VIEW_USER_TOOLBAR | 0x10 | 会员管理页按钮区 |
ADMIN_VIEW_COMMODITY_TOOLBAR | 0x7 | 商品管理按钮区 |
ADMIN_VIEW_COMMODITY_FOOTER | 0x6 | 商品管理底部 |
ADMIN_VIEW_CATEGORY_TOOLBAR | 0x701 | 分类管理按钮区 |
ADMIN_VIEW_ORDER_TOOLBAR | 0x13 | 订单管理按钮区 |
ADMIN_VIEW_ORDER_FOOTER | 0x12 | 订单管理底部 |
ADMIN_VIEW_CARD_TOOLBAR | 0x801 | 卡密管理按钮区 |
ADMIN_VIEW_CARD_FOOTER | 0x802 | 卡密管理底部 |
ADMIN_VIEW_CONFIG_TOOLBAR | 0x14 | 网站设置按钮区 |
菜单项示例:
#[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_HEADER | 0x128 | 前台头部 |
USER_VIEW_BODY | 0x129 | 前台 body |
USER_VIEW_FOOTER | 0x130 | 前台底部 |
USER_GLOBAL_VIEW_HEADER | 0x228 | 全局头部(含会员中心) |
USER_GLOBAL_VIEW_BODY | 0x229 | 全局 body |
USER_GLOBAL_VIEW_FOOTER | 0x230 | 全局底部 |
USER_VIEW_INDEX_HEADER | 0x10001 | 首页头部 |
USER_VIEW_INDEX_BODY | 0x10003 | 首页 body |
USER_VIEW_INDEX_FOOTER | 0x10004 | 首页底部 |
USER_VIEW_MENU | 0x57 | 会员中心菜单 |
USER_VIEW_HEADER_NAV | 0x88 | 商城顶栏导航(数组型,见下) |
USER_VIEW_AUTH_LOGIN_BUTTON | 0x41 | 登录按钮旁 |
USER_VIEW_AUTH_REGISTER_BUTTON | 0x42 | 注册按钮旁 |
USER_VIEW_SECURITY_NAV | 0x43 | 安全设置导航 |
USER_VIEW_PERSONAL_FORM | 0x44 | 个人资料表单 |
USER_VIEW_QUERY_TRADE_NO | 0x89 | 订单查询页 |
USER_VIEW_HEADER_NAV(0x88)和别的视图点位不同:它是数组型的,插件返回一个导航条目,由各主题自己渲染,而不是直接 echo HTML。这样换主题也不会破版。
内核与后台表格
| 常量 | 值 | 说明 |
|---|---|---|
KERNEL_INIT | 0x30 | 内核初始化完成,最早的介入点。想拦截整个请求就用它 |
HACK_ROUTE_TABLE_COLUMNS | 0x2005 | 给后台表格加列的唯一入口 |
HACK_ROUTE_TABLE_SEARCH | 0x2006 | 给后台表格加搜索条件 |
HACK_SUBMIT_FORM | 0x9038 | 给后台表单加字段 |
HACK_SUBMIT_TAB | 0x9039 | 给后台表单加标签页 |
USER_API_AUTH_LOGIN_BEGIN | 0x21 | 前台登录开始前 |
USER_API_AUTH_REGISTER_BEGIN | 0x19 | 前台注册开始前 |
数据型点位
这类点位带参数,参数按引用传递,改了就会影响后续流程。
订单与支付
| 常量 | 值 | 参数 |
|---|---|---|
USER_API_ORDER_TRADE_BEGIN | 0x16 | array $map 下单提交的原始数据 |
USER_API_ORDER_TRADE_PAY_BEGIN | 0x171 | Commodity $commodity, Order $order, Pay $pay |
USER_API_ORDER_TRADE_AFTER | 0x17 | Commodity $commodity, Order $order, Pay $pay |
USER_API_ORDER_PAY_AFTER | 0x18 | Commodity $commodity, Order $order, Pay $pay 付款完成 |
ORDER_MANUAL_DELIVERY_AFTER | 0x2200 | Order $order, bool $overwrite 手动发货写入之后 |
USER_API_RECHARGE_AFTER | 0x18191 | Recharge $recharge, Pay $pay 充值完成 |
SERVICE_PAY_CALLBACK_FAIL | 0x3010 | string $handle, string $reason, ?string $tradeNo, array $map |
SERVICE_PAY_CALLBACK_FAIL 的 $reason 取值:handle、not_found、credential、plugin、sign、status、duplicate、amount。其中 sign/amount/handle/credential 通常意味着有人在伪造回调,duplicate 是网关重复通知(正常现象,别报警)。
ORDER_MANUAL_DELIVERY_AFTER 触发时 $order->secret 已是新内容、delivery_status = 1;$overwrite 表示是不是覆盖了已有的发货内容。用来给买家补发「已发货」通知很合适。
账号
| 常量 | 值 | 参数 |
|---|---|---|
USER_API_AUTH_REGISTER_AFTER | 0x20 | User $user |
USER_API_AUTH_LOGIN_AFTER | 0x22 | User $user |
USER_API_AUTH_LOGIN_FAIL | 0x23 | string $account, string $reason |
ADMIN_API_AUTH_LOGIN_AFTER | 0x61 | Manage $manage |
ADMIN_API_AUTH_LOGIN_FAIL | 0x62 | string $email, string $reason |
前台失败 $reason:not_found、password、banned。 后台失败 $reason:throttled、captcha、not_found、password、totp、banned、shift、other(等待两步验证码不算失败)。
拿来做登录爆破告警正好。
前台数据
这些点位可以改写返回给前台的数据,做隐藏商品、改价展示、加字段都靠它们。
| 常量 | 值 | 参数 |
|---|---|---|
USER_API_INDEX_CATEGORY_LIST | 0x49 | array $category 分类列表 |
USER_API_INDEX_COMMODITY_LIST | 0x50 | array $data 商品列表 |
USER_API_INDEX_COMMODITY_DETAIL_INFO | 0x51 | array $item 商品详情 |
USER_API_INDEX_PAY_LIST | 0x53 | array $pay 可用支付方式 |
USER_API_INDEX_QUERY_LIST | 0x54 | array $data 订单查询结果 |
USER_API_INDEX_QUERY_SECRET | 0x55 | Order $order 查看卡密 |
USER_API_PURCHASE_RECORD_LIST | 0x56 | array $data 购买记录 |
商品与库存
| 常量 | 值 | 参数 |
|---|---|---|
COMMODITY_CHANGE_AFTER | 0x8100 | int[] $ids, string $action, ?Commodity $before |
CARD_CHANGE_AFTER | 0x8101 | int[] $commodityIds, string $reason |
SERVICE_SHOP_GET_ITEM_STOCK | 0x8000 | Commodity $commodity, string $race, array $sku |
COMMODITY_CHANGE_AFTER —— 商品新增、修改、删除、上下架、批量设置、对接同步之后触发,一律在数据库事务提交之后,拿到的一定是已落盘的变更。
$action 取值:create、update、delete、status、batch、sync。 $before 只在单商品保存路径提供修改前的模型,其余为 null。
批量路径(
status/batch)给的是请求里的 id 集合,可能包含实际没变化的商品 —— 订阅方要自己和快照比对算差量,别假设每个 id 都真的变了。delete触发时商品行已经没了,只能拿到 id。
CARD_CHANGE_AFTER —— 卡密池变化(也就是自动发货商品的库存变了)之后触发,同样在事务提交之后。
$commodityIds 是受影响的商品 id(不是卡密 id)。$reason 取值:import、edit、lock、unlock、sell、delete。
下单发货导致的库存下降不走这里,用
USER_API_ORDER_PAY_AFTER和ORDER_MANUAL_DELIVERY_AFTER。
工单
| 常量 | 值 | 参数 |
|---|---|---|
USER_API_TICKET_CREATE_AFTER | 0x2100 | Ticket $ticket, TicketMessage $message |
USER_API_TICKET_REPLY_AFTER | 0x2101 | Ticket $ticket, TicketMessage $message |
ADMIN_API_TICKET_REPLY_AFTER | 0x2102 | Ticket $ticket, TicketMessage $message, Manage $manage |
三个都在事务提交之后触发,钩子内抛异常不影响接口结果。
邮件
| 常量 | 值 | 参数 |
|---|---|---|
SERVICE_SMTP_SEND_BEFORE | 0x3000 | array $config, string $email, string $title, string $content |
SERVICE_SMTP_SEND_SUCCESS | 0x3001 | 同上 |
SERVICE_SMTP_SEND_ERROR | 0x3002 | 同上 |
SERVICE_SMTP_SEND_BEFORE 是唯一用返回值短路的点位:返回 true 表示邮件已由插件接管,核心不再走 SMTP。想把邮件换成其他通道(Telegram、企业微信)就用它。
内核与路由
| 常量 | 值 | 参数 |
|---|---|---|
CONTROLLER_CALL_BEFORE | 0x31 | object $controller, string $action |
CONTROLLER_CALL_AFTER | 0 | object $controller, string $action, mixed $result |
HTTP_ROUTE_RESPONSE | 0x47 | string $routePath, mixed $result |
HTTP_NOT_FOUND | 0x48 | string $routePath 路由未命中 |
RENDER_VIEW | 0x33 | string $result 渲染结果,可改写 HTML |
WAF_INTERCEPT | 0x289 | string $message WAF 拦截时 |
CSP_SOURCE_ALLOW | 0x8102 | array $sources CSP 放行源 |
LANG_MISS | 0x9100 | array $sourceList, array $langList 缺词条 |
ADMIN_API_PLUGIN_SAVE_CONFIG | 0x15 | int $id, array $map 插件配置保存时 |
HTTP_NOT_FOUND 拿来做扫描探测告警很好用 —— 短时间大量 404 基本就是有人在扫你。
风控 / 人工审核点位
0x2300 ~ 0x2305 是一组风控埋点。它们和别的钩子有两点不同,订阅前务必看完。
一、全部按引用传 RiskContext $risk,订阅方改对象、返回 void
千万不要 return true/false —— 派发器遇到 bool 会短路整条链,第一个返回 bool 的订阅方会把后面所有风控插件一起挡掉。
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->action:DENY 抛 JSONException,REVIEW 走各场景自己的挂起分支,LIMIT 核心不管。
二、为什么不能只靠抛异常
拒绝可以抛,但「挂人工审核」不行 —— 那需要核心知道「账号照建,但别给他签发会话」,光抛异常表达不了。
各点位
| 常量 | 值 | 参数 | 埋点位置 |
|---|---|---|---|
USER_API_AUTH_REGISTER_VALIDATED | 0x2300 | $risk, $user | 注册:校验后、落库前 |
USER_API_AUTH_PASSWORD_BEGIN | 0x2301 | $risk, $account | 找回密码:验证码校验之前 |
USER_API_RECHARGE_TRADE_BEGIN | 0x2302 | $risk, $user, $map | 充值下单:金额与通道校验之后 |
USER_API_CASH_SUBMIT_BEGIN | 0x2303 | $risk, $user, $map | 提现申请:绑定校验后、落库前 |
USER_API_TICKET_CREATE_BEGIN | 0x2304 | $risk, $user, $map | 工单创建:进 Service 之前 |
USER_API_ORDER_DELIVERY_BEGIN | 0x2305 | $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 元单、余额支付、各网关回调)。
钱已经收到了,此刻不该再谈「拒绝」,只该决定卡发不发 —— 所以订阅方只用 REVIEW:delivery_status 留 0、secret 换成提示文案,也就是手动发货商品在付款到发货之间的既有形态。被跳过的副作用(拉卡密、扣库存、分成与返利账单、发货邮件)一个都没执行过,所以审核通过后幂等地重跑一次恰好是对的。
已失效的点位
下面这 8 个常量还在 Hook.php 里,但核心已经没有任何地方调用它们了。订阅了不会报错,但永远不会被触发:
| 常量 | 值 | 用什么替代 |
|---|---|---|
ADMIN_VIEW_USER_TABLE | 0x8 | HACK_ROUTE_TABLE_COLUMNS |
ADMIN_VIEW_COMMODITY_TABLE | 0x5 | HACK_ROUTE_TABLE_COLUMNS |
ADMIN_VIEW_CATEGORY_TABLE | 0x702 | HACK_ROUTE_TABLE_COLUMNS |
ADMIN_VIEW_ORDER_TABLE | 0x11 | HACK_ROUTE_TABLE_COLUMNS |
ADMIN_VIEW_CATEGORY_POST | 0x703 | HACK_SUBMIT_FORM |
ADMIN_VIEW_COMMODITY_POST | 0x45 | HACK_SUBMIT_FORM |
USER_VIEW_COMMODITY_POST | 0x46 | — |
USER_API_INDEX_TRADE_CALC_AMOUNT | 0x52 | — |
网上能搜到的老教程还在教用
ADMIN_VIEW_USER_TABLE往后台表格加列(往里 echo 一段 JSON 列定义)。那套写法在当前版本已经完全无效。
给后台表格加列的正确姿势
现在唯一的入口是 HACK_ROUTE_TABLE_COLUMNS(0x2005),配合 Column 实体使用,而不是 echo 一段 JSON。
两个容易踩的点:
- 列渲染代码里
escapeHtml不是全局可用的,要自己处理转义 Order.amount是字符串,参与计算前先转换
