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 協議開源