Skip to content

支付外掛開發

支付外掛放在 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

比普通外掛多兩段:optionscallback

php
<?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 — 配置表單

站長在後臺 支付介面 裡填的那些欄位,由這個檔案定義。格式和通用外掛完全一樣,詳見外掛開發 · 配置表單

支付外掛通常按「基本配置 / 各種支付形態」分成多個頁籤:

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
<?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_REDIRECT2直接跳轉到 setUrl() 的地址
TYPE_LOCAL_RENDER3用外掛的 View/<code>.html 本地渲染支付頁(比如自己畫二維碼)
TYPE_SUBMIT4用 POST 表單提交過去

Impl/Signature.php

IS_SIGNtrue 時必須有這個類,實現 verification()

php
<?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 鉤子。

核心的回撥處理流程

回撥進來後,核心按順序做這些事,全部透過才會給訂單發貨:

  1. Config/Info.phpcallback 定義 —— 沒有就拒(plugin
  2. 如果外掛有 Vendor/autoload.php,先載入
  3. IS_SIGN 為真 → 檢查憑據是否配置(沒配拒,credential)→ 調 Signature::verification()(失敗拒,sign
  4. IS_STATUS 為真 → 比對狀態欄位(不符拒,status
  5. 取出訂單號和金額,交給訂單服務
  6. 處理完把 FIELD_RESPONSE 的內容原樣回給閘道器

各種失敗原因都會帶著 $reason 觸發 SERVICE_PAY_CALLBACK_FAIL0x3010)鉤子,見 Hook 鉤子大全。做支付告警外掛訂閱它就行。

金額與貨幣

站點用非人民幣定價時,提交給閘道器的金額要按匯率換算。匯率的含義是**「1 個站點貨幣值多少人民幣」**。

前臺展示金額時別寫死 ¥ 和「元」,用:

php
\App\Util\Currency::symbol()

寫死貨幣符號是支付外掛裡最常見的顯示 bug —— 扣款金額是對的,但顯示出來的幣種是錯的。

私有依賴

外掛要用第三方 SDK,放到自己的 Vendor/ 下並提供 autoload.php,核心在處理回撥前會自動 require 它。不要去動全域性的 composer.json

測試

後臺 支付介面 裡有回撥測試功能(callbackTest),可以在不真實付款的情況下驗證回撥鏈路。

基於 MIT 協議開源