跳轉到主要內容

易支付接口對接指南:MD5 簽名、異步通知與訂單超時的三個坑

易支付(epay)協議的 submit.php、mapi.php 和 api.php 怎麼對接?本文講清簽名算法、為什麼只通知支付成功、訂單有效期該怎麼設,以及驗簽失敗和收不到通知的排查方法。

發佈於 約 5 分鐘閲讀

手繪插畫:一張寫着 pid、money、sign 參數的易支付請求單,旁邊的人指着它講解
本文目錄
  1. 易支付協議由哪幾個接口組成?
  2. 易支付的簽名怎麼算?
  3. 為什麼易支付的支付通知只在成功時發送?
  4. 收到支付通知後應該怎麼處理?
  5. 訂單有效期為什麼要比對接系統的超時短?
  6. 查詢訂單接口為什麼建議用 POST?
  7. 付款成功了,對接系統卻沒收到通知?
  8. 什麼時候不該用易支付?

易支付(epay)是很多自建站系統內置的一套支付協議:V2Board、SSPanel、獨角數卡,以及 Xboard 自帶的「EPay」支付方式都支持它。對接方只需要填四樣東西——網關地址、商戶 ID(pid)、商戶密鑰(key)和支付通知地址(notify_url)——就能收款,不用寫代碼。

協議本身很簡單,真正容易出問題的是三個地方:簽名串的拼法、支付通知只代表成功,以及訂單有效期與對接系統的超時不一致。下文以 KukoPay 的 易支付兼容接口 為例逐一説明,規則對其他易支付服務商也基本通用。

易支付協議由哪幾個接口組成?

一套完整的易支付對接通常只用到三個地址,都拼在網關地址後面:

地址方式用途
/submit.phpGET 或表單 POST頁面跳轉支付:買家瀏覽器帶着簽名參數訪問,被重定向到收銀台
/mapi.phpPOST服務端下單:返回收銀台地址 payurl,由你把買家帶過去
/api.php?act=orderPOST(推薦)或 GET查詢單筆訂單狀態

在 KukoPay,網關地址是 https://api.kukopay.com/epay。對接系統會自己在後面拼 /submit.php 等路徑,所以設置裏只填到 /epay 為止。

下單參數裏最關鍵的是 out_trade_no(你的訂單號)、money(金額,最多兩位小數)、notify_url 和 sign。money 只有數字沒有幣種,所以收款幣種由商戶在控制台為這個 pid 選定。

易支付的簽名怎麼算?

簽名方法和彩虹易支付 SDK 一致,四步:

  1. 去掉 sign、sign_type 和值為空的參數;
  2. 按參數名的 ASCII 順序排序;
  3. 拼成 a=1&b=2 的形式,值不做 URL 編碼;
  4. 在末尾直接接上商戶密鑰,求 MD5,取 32 位小寫。
function epay_sign(array $params, string $key): string {
    ksort($params);
    $pairs = [];
    foreach ($params as $k => $v) {
        if ($k === 'sign' || $k === 'sign_type' || $v === '' || $v === null) continue;
        $pairs[] = $k . '=' . $v;
    }
    return md5(implode('&', $pairs) . $key);
}

簽名校驗失敗時,按出現頻率從高到低檢查:

  • 密鑰或 pid 填錯了環境。 沙箱和正式環境各有一套 pid 和密鑰,混用必然失敗。
  • 值被 URL 編碼了。 比如 name=Pro%20plan 和 name=Pro plan 算出來的簽名不同,協議要求用原值。
  • 把 sign_type 也拼進去了,或者忘了排序。
  • 空值的處理。 有些系統用 PHP 的 http_build_query 拼簽名串,會把空值也拼進去。KukoPay 對這兩種寫法都能驗簽通過,但換到別的服務商時值得留意。

為什麼易支付的支付通知只在成功時發送?

這是易支付對接裏最反直覺、也最重要的一點:KukoPay 只在訂單支付成功時發送通知,失敗和過期都不發。

原因在客戶端。很多易支付客戶端(包括 Xboard 自帶的 EPay)收到通知後只校驗簽名,不看 trade_status。如果平台把「支付失敗」也簽名發過去,這些系統會把它當成支付成功,直接給用戶開通服務。對於這類客戶端,「只通知成功」是唯一安全的做法。

所以:

  • 收到通知 ⇒ 這筆訂單已經支付成功(前提是驗簽通過);
  • 沒收到通知 ⇒ 不代表失敗,可能還在支付中、已過期,或者通知還在重試;
  • 需要知道失敗或過期,請用查詢接口 api.php?act=order,status 為 1 表示已支付,2 表示已全額退款,0 表示未支付、已失敗或已過期。

收到支付通知後應該怎麼處理?

通知是一個 GET 請求,參數在查詢字符串裏。處理順序:

  1. 先驗簽,算法同上。簽名使用的是發送時刻的商戶密鑰。
  2. 按 out_trade_no 去重。 同一筆訂單的通知可能重複到達,已經處理過的直接返回 success。
  3. 核對金額。 用通知裏的 money 和你自己訂單的金額比對,不一致就不要發貨。
  4. 響應正文必須是 success 這幾個字母,HTTP 狀態碼為 2xx,並且在 10 秒內返回。

返回別的內容、非 2xx 或超時,都算失敗。KukoPay 會依次間隔 30 秒、2 分鐘、10 分鐘、30 分鐘、2 小時重試,最多共發送 6 次。修好接收端之後,還可以在控制台「開發者 → Webhook」的投遞記錄裏手動重發,不需要買家重新付款。

不要在返回 success 之前做耗時的事情,比如發郵件、調用第三方接口。超過 10 秒會被判為失敗並重試,你的系統就會收到第二次通知——這正是需要按訂單號去重的原因。

訂單有效期為什麼要比對接系統的超時短?

易支付協議沒有表示「訂單何時失效」的參數,但對接系統自己通常會取消長時間未支付的訂單:Xboard 和 V2Board 是 2 小時,獨角數卡默認 5 分鐘。

問題出在兩邊的時鐘不一致時。假設收銀台開放 24 小時,而對接系統 2 小時後就取消了訂單:買家第 3 小時付款成功,平台照常發出通知,對接系統找不到這筆「已取消」的訂單,可能直接回復 success 卻不發貨——錢收了,貨沒給,只能人工退款。

所以 KukoPay 要求商戶在控制台為每個環境設置一個訂單有效期(5 分鐘到 24 小時,默認 30 分鐘),並建議設成不超過對接系統自己的超時時間:

  • 設短了,最壞情況是買家需要重新下單;
  • 設長了,才可能收到已取消訂單的錢;
  • 拿不準時寧可設短。

這個問題不只存在於易支付。KukoPay 的 Xboard 插件走原生 API,會主動把收銀台的關閉時間設為 Xboard 訂單創建後 1 小時 50 分,比 Xboard 取消訂單早 10 分鐘,原理相同。

查詢訂單接口為什麼建議用 POST?

api.php?act=order 按協議直接用商戶密鑰 key 作為憑證。協議允許把參數放在 GET 查詢字符串裏,但 URL 會被反向代理、CDN 和瀏覽器歷史原樣記錄,密鑰就跟着留在了那些日誌裏。

curl -X POST https://api.kukopay.com/epay/api.php \
  -d act=order -d pid=10001 -d key="$EPAY_KEY" -d out_trade_no=XB2026…

只從服務端調用,並且把參數放在請求體裏。

付款成功了,對接系統卻沒收到通知?

到控制台「開發者 → Webhook」查看這筆訂單的投遞記錄:

  • 對方返回了內容,但不是 success:接收端報錯或驗簽失敗,看返回內容裏的錯誤信息。
  • 連接失敗或超時:檢查對接系統的防火牆、CDN 或 WAF 是否攔截了來自 KukoPay 的請求。
  • 沒有投遞記錄:確認訂單確實是通過易支付下的,並且已經支付成功。

什麼時候不該用易支付?

易支付兼容接口是為已經支持這個協議的系統準備的。如果你在開發一個新系統,建議直接對接 API:

  • 易支付使用 MD5 簽名,API 的 Webhook 使用 HMAC-SHA256,並支持密鑰輪換期間新舊密鑰同時簽名;
  • 易支付只能表達「支付成功」這一個結果,API 有失敗、過期、退款、拒付等完整事件;
  • 易支付協議本身沒有退款、對賬和拒付舉證,這些要到控制台操作,或者另外調用 API。

兩種方式創建的訂單完全相同,走同一個收銀台和同一套結算流程,所以先用易支付跑起來、以後再遷移到 API 也沒有問題。

想先試一試?在 沙箱控制台 開通易支付,用測試卡 4242 4242 4242 4242 下一單,確認對接系統收到通知、訂單變成已支付。沙箱不需要等待資質審核。

在沙箱裏把它跑通

註冊商戶後即可申請測試密鑰,不需要等待資質審核。