首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 整合 99Bill (快錢) 支付網關完整實作 (Zend Framework)

     編輯:PHP 相關知識

架構概覽

  • Request:商戶 → 99Bill (用戶付款頁)
  • Background Return (bgUrl):99Bill → 商戶後台 (Server-to-Server,異步通知)
  • Page Return (pageUrl):99Bill → 商戶前台 (用戶瀏覽器重導向,同步通知)

核心參數說明

參數說明必要性
merchantAcctId商戶號 (11位+01 或 16位)必填
bgUrl後台通知地址 (絕對 URL)必填
orderId商戶訂單號 (唯一)必填
orderAmount訂單金額 (分為單位)必填
orderTime訂單時間 (yyyyMMddHHmmss)必填
signType簽名類型 (4=PKI)必填
signMsg簽名字串 (PKI 簽名)必填
payType支付方式 (00=全部)必填
inputCharset編碼 (1=UTF-8)必填
version網關版本 (v2.0)必填
language語言 (1=中文)必填
ext1, ext2擴展欄位 (原樣返回)選填

Model: BillRequest (發送請求建構)

<?php
class Application_Model_BillRequest
{
    public $merchantAcctId = '1001011111101';
    public $bgUrl = 'https://yourdomain.com/payment/99bill/notify';
    public $orderId;
    public $orderAmount;
    public $orderTime;
    public $payerName = '';
    public $payerContactType = '';
    public $payerContact = '';
    public $productName = 'TOLPC';
    public $productNum = '1';
    public $productId;
    public $productDesc = '';
    public $payType = '00';
    public $inputCharset = '1';
    public $version = 'v2.0';
    public $language = '1';
    public $signType = '4';
    public $pageUrl = '';
    public $ext1;
    public $ext2;
    public $bankId = '';
    public $redoFlag = '';
    public $pid = '';
    public $signMsg;

    public function __construct(array $orderData)
    {
        // 映射訂單資料
        $this->orderId     = 'TOLPC' . sprintf('%09d', $orderData['orderId']);
        $this->orderAmount = $orderData['orderAmount'];  // 分
        $this->orderTime   = date('YmdHis', $orderData['orderTime']);
        $this->productId   = $orderData['ets_license'];
        $this->ext1        = $orderData['orderId'];      // 回傳辨識用
        $this->ext2        = $orderData['orderTime'];

        // 產生簽名
        $this->signMsg = $this->generateSignMsg();
    }

    private function generateSignMsg(): string
    {
        // 1. 收集非空參數
        $params = [];
        foreach ($this as $key => $value) {
            if ($key === 'signMsg') continue;
            if ($value !== '' && $value !== null) {
                $params[$key] = $value;
            }
        }

        // 2. 依鍵名 ASCII 排序 (99Bill 要求)
        ksort($params);

        // 3. 組裝查詢字串
        $queryString = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
        $queryString = urldecode($queryString);  // 99Bill 要求不編碼

        // 4. PKI 簽名 (私鑰 SHA1 簽名 + Base64)
        $privKey = openssl_get_privatekey(file_get_contents('99bill-rsa.pem'));
        openssl_sign($queryString, $signature, $privKey, OPENSSL_ALGO_SHA1);
        openssl_free_key($privKey);

        return base64_encode($signature);
    }

    public function toArray(): array
    {
        $arr = [];
        foreach ($this as $key => $value) {
            if ($value !== '' && $value !== null) {
                $arr[$key] = $value;
            }
        }
        return $arr;
    }
}

Controller: 發起支付

<?php
class PaymentController extends Zend_Controller_Action
{
    public function payAction()
    {
        // 1. 取得訂單資料 (從 DB、Session 等)
        $orderData = [
            'orderId'     => 100000125,
            'orderAmount' => 31100,  // 分
            'orderTime'   => time(),
            'ets_license' => '1',
        ];

        // 2. 建構請求
        $billRequest = new Application_Model_BillRequest($orderData);

        // 3. 渲染自動提交表單
        $this->view->billRequest = $billRequest->toArray();
    }
}

View: 自動提交表單 (payment/pay.phtml)

<form name="kqPay" action="https://www.99bill.com/gateway/recvMerchantInfoAction.htm" method="post">
    <?php foreach ($this->billRequest as $key => $val): ?>
        <input type="hidden" name="<?= $key ?>" value="<?= htmlspecialchars($val) ?>" />
    <?php endforeach; ?>
    <noscript><input type="submit" value="前往付款" /></noscript>
</form>
<script>document.kqPay.submit();</script>

Model: BillResponse (接收通知驗證)

<?php
class Application_Model_BillResponse
{
    public $merchantAcctId, $version, $language, $signType, $payType, $bankId;
    public $orderId, $orderTime, $orderAmount, $dealId, $bankDealId, $dealTime;
    public $payAmount, $fee, $ext1, $ext2, $payResult, $errCode, $signMsg;

    public function __construct(array $data)
    {
        foreach ($data as $key => $value) {
            if (property_exists($this, $key)) {
                $this->$key = $value;
            }
        }
    }

    public function verifySign(): bool
    {
        // 1. 收集非空參數 (不含 signMsg)
        $params = [];
        foreach ($this as $key => $value) {
            if ($key === 'signMsg') continue;
            if ($value !== '' && $value !== null) {
                $params[$key] = $value;
            }
        }
        ksort($params);
        $queryString = urldecode(http_build_query($params, '', '&', PHP_QUERY_RFC3986));

        // 2. 公鑰驗證
        $pubKey = openssl_get_publickey(file_get_contents('99bill-rsa.cer'));
        $result = openssl_verify($queryString, base64_decode($this->signMsg), $pubKey);
        openssl_free_key($pubKey);

        return $result === 1;
    }

    public function isSuccess(): bool
    {
        return $this->payResult === '10';  // 10=成功, 11=失敗
    }

    public function getOrderId(): string
    {
        return str_replace('TOLPC', '', $this->orderId);
    }
}

Controller: 接收異步通知 (bgUrl)

<?php
class PaymentController extends Zend_Controller_Action
{
    public function notifyAction()
    {
        // 1. 接收原始 POST
        $response = new Application_Model_BillResponse($_POST);

        // 2. 驗證簽名
        if (!$response->verifySign()) {
            error_log('99Bill 簽名驗證失敗: ' . json_encode($_POST));
            echo '0https://yourdomain.com/payment/fail';
            return;
        }

        // 3. 判斷支付結果
        if ($response->isSuccess()) {
            $orderId = $response->getOrderId();
            
            // 4. 業務處理 (更新訂單狀態、發貨等) - 注意冪等性
            $this->processPaymentSuccess($orderId, $response->toArray());
            
            // 5. 回應 99Bill (XML 格式)
            echo '1https://yourdomain.com/payment/success';
        } else {
            // 支付失敗處理
            $this->processPaymentFail($response->getOrderId(), $response->errCode);
            echo '1https://yourdomain.com/payment/fail';
        }
    }

    private function processPaymentSuccess(string $orderId, array $data)
    {
        // 使用資料庫交易 + 鎖防止重複處理
        // 檢查訂單狀態、金額核對、更新狀態、記錄支付流水
    }
}

Controller: 同步跳轉 (pageUrl)

<?php
public function returnAction()
{
    $response = new Application_Model_BillResponse($_GET);  // pageUrl 為 GET

    if ($response->verifySign() && $response->isSuccess()) {
        $this->view->message = '支付成功,訂單處理中...';
        $this->render('success');
    } else {
        $this->view->message = '支付失敗或驗證失敗';
        $this->render('fail');
    }
}

關鍵安全與可靠性提醒

  • 🔐 簽名驗證不可省略:防止偽造通知、金額竄改
  • 🔐 私鑰 (99bill-rsa.pem) 嚴禁洩露:放在 Web 根目錄外、限制檔案權限
  • 🔁 異步通知冪等性:同一訂單可能收到多次通知,需檢查狀態避免重複發貨
  • 💰 金額核對:驗證 payAmount === orderAmount,防止惡意修改金額
  • 🕐 時間同步:伺服器時間需與標準時間同步 (NTP),避免簽名時間誤差
  • 📝 完整日誌:記錄所有通知原始資料、驗證結果、業務處理結果
  • 🧪 測試環境:使用 99Bill 測試環境網關 https://sandbox.99bill.com/... 及測試商戶號

憑證檔案說明

  • 99bill-rsa.pem:商戶私鑰 (簽名用),對應公鑰在 99Bill 端
  • 99bill-rsa.cer:99Bill 公鑰 (驗證用),對應私鑰在 99Bill 端
  • 兩者非同一組金鑰對,各自保管自己的私鑰,交換公鑰
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved