支付外掛開發
支付外掛放在 app/Pay/<外掛名>/ 下。目錄名就是 handle,全系統靠它識別這個支付方式。
目錄結構
app/Pay/Demo/
Config/
Info.php 外掛資訊 + 回撥規則(必需)
Config.php 預設配置值
Submit.js 後臺「支付介面」裡的表單(也可用舊格式 Submit.php)
Impl/
Pay.php 下單實現(必需)
Signature.php 回撥簽名校驗(開了驗簽就必需)
View/
1.html 本地渲染支付頁時用的模板
Assets/ 外掛自己的靜態資源
Vendor/
autoload.php 外掛私有的第三方依賴(可選)Config/Info.php
比普通外掛多兩段:options 和 callback。
<?php
declare(strict_types=1);
return [
'version' => '1.0.0',
'name' => '演示支付',
'author' => '你的名字',
'website' => 'https://example.com',
'description' => '一句話說明',
// 這個外掛支援幾種支付形態,站長在後臺選一種
'options' => [
1 => '掃碼支付',
2 => 'PC 支付',
3 => 'WAP 支付',
],
// 回撥怎麼校驗,核心照著這份定義自動處理
'callback' => [
\App\Consts\Pay::IS_SIGN => true, // 是否驗簽
\App\Consts\Pay::IS_STATUS => true, // 是否校驗狀態欄位
\App\Consts\Pay::FIELD_STATUS_KEY => 'trade_status', // 狀態欄位名
\App\Consts\Pay::FIELD_STATUS_VALUE => 'TRADE_SUCCESS', // 成功時的值
\App\Consts\Pay::FIELD_ORDER_KEY => 'out_trade_no', // 訂單號欄位名
\App\Consts\Pay::FIELD_AMOUNT_KEY => 'total_amount', // 金額欄位名
\App\Consts\Pay::FIELD_RESPONSE => 'success' // 處理完回給閘道器的內容
]
];
callback這段少了,回撥會直接被拒,理由是「外掛缺少 Config/Info.php 的 callback 定義」。
Config/Submit.js — 配置表單
站長在後臺 支付介面 裡填的那些欄位,由這個檔案定義。格式和通用外掛完全一樣,詳見外掛開發 · 配置表單。
支付外掛通常按「基本配置 / 各種支付形態」分成多個頁籤:
[
{
name: `${util.icon("/app/Pay/Demo/Assets/Icon/Setting.png")} 基本配置`,
form: [
{ title: "商戶號", name: "mch_id", type: "input", placeholder: "支付平臺分配的商戶號", required: true },
{ title: "API 金鑰", name: "key", type: "input", placeholder: "支付平臺分配的金鑰", required: true }
]
},
{
name: "掃碼支付",
form: [
{ title: "收款方", name: "payee", type: "input", placeholder: "收銀臺中顯示的收款方" }
]
}
]分組的
name支援util.icon(),可以傳 Font Awesome 類名,也可以傳外掛自己Assets/下的圖片路徑。
舊格式 Config/Submit.php(扁平欄位陣列)仍然可用,兩個檔案同時存在時 Submit.js 優先。
Impl/Pay.php
實現 App\Pay\Pay 介面,只有一個方法 trade():
<?php
declare(strict_types=1);
namespace App\Pay\Demo\Impl;
use App\Entity\PayEntity;
use App\Pay\Base;
use Kernel\Exception\JSONException;
class Pay extends Base implements \App\Pay\Pay
{
public function trade(): PayEntity
{
// $this->code 就是站長在後臺選的那個 options 鍵
if ($this->code == 1) {
return $this->qrcode();
}
throw new JSONException("非法請求");
}
private function qrcode(): PayEntity
{
// 用 $this->config 裡的憑據去請求支付平臺,拿到支付連結
$payUrl = '...';
$entity = new PayEntity();
$entity->setType(\App\Pay\Pay::TYPE_REDIRECT);
$entity->setUrl($payUrl);
return $entity;
}
}基類給你的東西
繼承 App\Pay\Base 後可以直接用:
| 屬性 | 說明 |
|---|---|
$this->amount | 訂單金額(float) |
$this->tradeNo | 訂單號 |
$this->config | 站長在後臺填的配置 |
$this->callbackUrl | 非同步回撥地址,交給支付平臺 |
$this->returnUrl | 使用者付完跳回來的地址 |
$this->clientIp | 買家 IP |
$this->code | 站長選的支付形態(對應 options 的鍵) |
$this->handle | 外掛目錄名 |
三種支付呈現方式
PayEntity::setType() 決定前臺怎麼把使用者送到支付頁:
| 常量 | 值 | 行為 |
|---|---|---|
TYPE_REDIRECT | 2 | 直接跳轉到 setUrl() 的地址 |
TYPE_LOCAL_RENDER | 3 | 用外掛的 View/<code>.html 本地渲染支付頁(比如自己畫二維碼) |
TYPE_SUBMIT | 4 | 用 POST 表單提交過去 |
Impl/Signature.php
IS_SIGN 為 true 時必須有這個類,實現 verification():
<?php
declare(strict_types=1);
namespace App\Pay\Demo\Impl;
class Signature
{
public function verification(array $map, array $config): bool
{
$sign = $map['sign'] ?? '';
unset($map['sign']);
ksort($map);
$expect = md5(urldecode(http_build_query($map) . '&key=' . $config['app_key']));
return hash_equals($expect, (string)$sign);
}
}返回 false 核心就會拒掉這次回撥,並觸發 SERVICE_PAY_CALLBACK_FAIL 鉤子。
核心的回撥處理流程
回撥進來後,核心按順序做這些事,全部透過才會給訂單發貨:
- 讀
Config/Info.php的callback定義 —— 沒有就拒(plugin) - 如果外掛有
Vendor/autoload.php,先載入 IS_SIGN為真 → 檢查憑據是否配置(沒配拒,credential)→ 調Signature::verification()(失敗拒,sign)IS_STATUS為真 → 比對狀態欄位(不符拒,status)- 取出訂單號和金額,交給訂單服務
- 處理完把
FIELD_RESPONSE的內容原樣回給閘道器
各種失敗原因都會帶著 $reason 觸發 SERVICE_PAY_CALLBACK_FAIL(0x3010)鉤子,見 Hook 鉤子大全。做支付告警外掛訂閱它就行。
金額與貨幣
站點用非人民幣定價時,提交給閘道器的金額要按匯率換算。匯率的含義是**「1 個站點貨幣值多少人民幣」**。
前臺展示金額時別寫死 ¥ 和「元」,用:
\App\Util\Currency::symbol()寫死貨幣符號是支付外掛裡最常見的顯示 bug —— 扣款金額是對的,但顯示出來的幣種是錯的。
私有依賴
外掛要用第三方 SDK,放到自己的 Vendor/ 下並提供 autoload.php,核心在處理回撥前會自動 require 它。不要去動全域性的 composer.json。
測試
後臺 支付介面 裡有回撥測試功能(callbackTest),可以在不真實付款的情況下驗證回撥鏈路。
