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是字串,參與計算前先轉換
