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

ECSHOP 接入交通銀行網銀支付插件開發全記錄

     編輯:PHP 相關知識

項目背景

ECSHOP 官方未內置交通銀行(BankComm)支付插件。項目需對接交行 B2C 網銀支付接口(版本 1.0.0.0),採用雙向 Socket 通訊 + XML 報文 + 數字簽名模式。本文記錄插件核心邏輯、關鍵代碼與踩坑點,供同類二次開發參考。

接口流程簡述

  1. 下單請求:商戶系統組裝參數 → 生成待簽名串 → 發送至交行前置機(TCP 8080) → 解析返回 XML 獲取 orderUrl 和 signMsg → 生成提交表單頁面導向交行支付頁。
  2. 用戶支付:用戶在交行頁面完成支付。
  3. 異步通知/同步返回:交行 POST notifyMsg 到商戶 respond.php → 商戶再次發送驗證報文至前置機 → 解析結果 → 更新訂單狀態。

目錄結構

includes/modules/payment/bankcomm.php      # 支付插件主類
languages/zh_cn/payment/bankcomm.php       # 語言包
respond.php                                # 異步通知/同步返回入口(需放在網站根目錄)

核心代碼:bankcomm.php

<?php
/**
 * 交通銀行在線支付插件 For ECSHOP
 * Author: Reson
 * Date: 2014/03/31
 */
if (!defined('IN_ECS')) die('Hacking attempt');

// 載入語言包
$payment_lang = ROOT_PATH . 'languages/' . $GLOBALS['_CFG']['lang'] . '/payment/bankcomm.php';
if (file_exists($payment_lang)) {
    global $_LANG;
    include_once($payment_lang);
}

/* 模塊元信息:後台支付方式列表顯示用 */
if (isset($set_modules) && $set_modules == TRUE) {
    $i = isset($modules) ? count($modules) : 0;
    $modules[$i]['code']        = basename(__FILE__, '.php');
    $modules[$i]['desc']        = 'bankcomm_desc';
    $modules[$i]['is_cod']      = '0';
    $modules[$i]['is_online']   = '1';
    $modules[$i]['pay_fee']     = '0';
    $modules[$i]['author']      = 'Reson';
    $modules[$i]['website']     = 'https://www.jb51.net';
    $modules[$i]['version']     = '1.0.0.0';
    $modules[$i]['config']      = array(); // 如需商戶號、密鑰等可在這裡定義輸入框
    return;
}

/**
 * 支付類
 */
class bankcomm {
    /** 構造函數兼容 PHP4/5 */
    function bankcomm() {}
    function __construct() { $this->bankcomm(); }

    /**
     * 生成支付表單(用戶點擊「確認支付」後調用)
     * @param array $order 訂單信息 ['order_id','order_sn','order_amount','log_id'...]
     * @return string HTML 表單代碼
     */
    function get_code($order) {
        // 1. 組裝業務參數(嚴格按接口文檔順序)
        $param = [
            'interfaceVersion' => '1.0.0.0',
            'merID'            => '301310063009501', // 測試商戶號,生產環境需替換
            'orderid'          => $order['order_sn'],
            'orderDate'        => local_date('Ymd', gmtime()),
            'orderTime'        => local_date('His', gmtime()),
            'tranType'         => '0',               // 0: B2C
            'amount'           => $order['order_amount'],
            'curType'          => 'CNY',
            'orderContent'     => '',
            'orderMono'        => $order['order_sn'], // 商戶備註
            'phdFlag'          => '',
            'notifyType'       => '1',               // 1: 異步通知
            'merURL'           => '',
            'goodsURL'         => $order['goods_url'], // 支付成功跳轉頁
            'jumpSeconds'      => '',
            'payBatchNo'       => '',
            'proxyMerName'     => '',
            'proxyMerType'     => '',
            'proxyMerCredentials' => '',
            'netType'          => '0',               // 0: HTML 渠道
            'issBankNo'        => '',
        ];

        // 2. 生成待簽名源串(issBankNo 不參與簽名)
        $source = '';
        foreach ($param as $k => $v) {
            if ($k !== 'issBankNo') $source .= $v . '|';
        }
        $source = rtrim($source, '|');

        // 3. 與前置機通訊獲取簽名與支付頁 URL
        $tranCode = 'cb2200_sign';
        $socketUrl = 'tcp://127.0.0.1:8080'; // 前置機地址,按實際部署修改
        $fp = @stream_socket_client($socketUrl, $errno, $errstr, 30);
        if (!$fp) {
            return "連接前置機失敗:$errstr ($errno)";
        }

        $xmlReq = ""
                . ""
                . "$tranCode"
                . "$source"
                . "";
        fwrite($fp, $xmlReq);

        $retMsg = '';
        while (!feof($fp)) $retMsg .= fgets($fp, 1024);
        fclose($fp);

        // 4. 解析返回 XML
        $dom = new DOMDocument();
        $dom->loadXML($retMsg);
        $retCode = $dom->getElementsByTagName('retCode')->item(0)->nodeValue;
        $errMsg  = $dom->getElementsByTagName('errMsg')->item(0)->nodeValue;
        $signMsg = $dom->getElementsByTagName('signMsg')->item(0)->nodeValue;
        $orderUrl= $dom->getElementsByTagName('orderUrl')->item(0)->nodeValue;
        $merID   = $dom->getElementsByTagName('MerchID')->item(0)->nodeValue;

        if ($retCode !== '0') {
            return "交行下單失敗:$errMsg";
        }

        // 5. 生成自動提交表單
        $param['signMsg_value'] = $signMsg;
        $param['orderUrl_value'] = $orderUrl;
        return $this->create_html($param);
    }

    /** 構建隱藏表單並自動提交 */
    function create_html($p) {
        $fields = [
            'interfaceVersion','merID','orderid','orderDate','orderTime','tranType',
            'amount','curType','orderContent','orderMono','phdFlag','notifyType',
            'merURL','goodsURL','jumpSeconds','payBatchNo','proxyMerName',
            'proxyMerType','proxyMerCredentials','netType','merSignMsg','issBankNo'
        ];
        $html = '
'; foreach ($fields as $f) { $val = isset($p[$f]) ? $p[$f] : ''; if ($f === 'merSignMsg') $val = $p['signMsg_value']; $html .= ''; } $html .= ''; $html .= '
'; return $html; } /** * 異步通知/同步返回處理入口 * 放在 respond.php 中實例化調用:$pay = new bankcomm(); $pay->respond(); * @return bool true=驗證成功並更新訂單 */ function respond() { $tranCode = 'cb2200_verify'; $notifyMsg = $_REQUEST['notifyMsg'] ?? ''; if (!$notifyMsg) return false; // 分離簽名與原文(最後一個 | 後為簽名) $lastPos = strrpos($notifyMsg, '|'); if ($lastPos === false) return false; $signMsg = substr($notifyMsg, $lastPos + 1); $srcMsg = substr($notifyMsg, 0, $lastPos + 1); // 發送驗證報文 $socketUrl = 'tcp://127.0.0.1:8080'; $fp = @stream_socket_client($socketUrl, $errno, $errstr, 30); if (!$fp) return false; $xmlReq = "" . "" . "$tranCode" . "$notifyMsg" . ""; fwrite($fp, $xmlReq); $retMsg = ''; while (!feof($fp)) $retMsg .= fgets($fp, 1024); fclose($fp); // 解析驗證結果 $dom = new DOMDocument(); $dom->loadXML($retMsg); $retCode = $dom->getElementsByTagName('retCode')->item(0)->nodeValue; $errMsg = $dom->getElementsByTagName('errMsg')->item(0)->nodeValue; if ($retCode !== '') { // 交行文檔:retCode 為空表示驗簽成功 error_log("交行驗簽失敗:$errMsg | notifyMsg: $notifyMsg"); return false; } // 解析原文獲取訂單號(按字段順序,orderid 為第 2 個字段,索引 1) $arr = preg_split('/\|{1,}/', $srcMsg); $orderSn = $arr[1] ?? ''; $actionNote = base64_decode($arr[16] ?? ''); // 更新訂單狀態(ECSHOP 內置函數) if ($orderSn) { order_paid($orderSn, PS_PAYED, $actionNote); return true; } return false; } } ?>

語言包:languages/zh_cn/payment/bankcomm.php

<?php
global $_LANG;
$_LANG['bankcomm']       = '交通銀行在線支付';
$_LANG['bankcomm_desc']  = '支持交通銀行借記卡/信用卡網銀支付';
$_LANG['pay_button']     = '交通銀行支付';
?>

異步通知入口:respond.php(放置於網站根目錄)

<?php
define('IN_ECS', true);
require_once(dirname(__FILE__) . '/includes/init.php');
require_once(ROOT_PATH . 'includes/modules/payment/bankcomm.php');

$pay = new bankcomm();
$result = $pay->respond();

if ($result) {
    echo 'SUCCESS'; // 交行要求返回 SUCCESS 字符串確認接收
} else {
    echo 'FAIL';
}
?>

前台調用示例(如 flow.php 中)

include_once(ROOT_PATH . 'includes/modules/payment/bankcomm.php');
$order = [
    'order_sn'    => $order['order_sn'],
    'order_amount'=> $order['order_amount'],
    'goods_url'   => 'https://' . $_SERVER['HTTP_HOST'] . '/respond.php?code=bankcomm',
];
$pay = new bankcomm();
$smarty->assign('pay_button', $pay->get_code($order));

關鍵踩坑總結

  1. 前置機部署:交行提供的前置機程序(Windows/Linux 均有)必須先正確安裝、配置端口(默認 8080)、導入商戶證書、啟動服務。插件只是客戶端。
  2. 字段順序嚴格:組裝 $source 時必須完全按照接口文檔規定的字段順序,否則簽名驗證失敗。
  3. 編碼統一 UTF-8:請求/響應 XML、Socket 傳輸、數據庫連接均需 UTF-8,避免中文亂碼導致簽名不一致。
  4. 金額單位:接口文檔多為「元」,保留兩位小數,不帶逗號,如 100.00。
  5. 測試環境與生產環境隔離:商戶號、前置機 IP、證書文件完全不同,建議通過後台配置項區分,避免硬編碼。
  6. 異步通知幂等性:respond() 可能被多次調用(網絡重試),內部 order_paid() 已有判重邏輯,但建議自行記錄處理日誌防重。
  7. 防火牆/安全組:服務器需放行出站 8080 端口(訪問前置機),以及入站 80/443 端口(接收交行通知)。

擴展建議

  • 將 Socket 通訊封裝為獨立類,支援超時重試、長連接池、異常熔斷。
  • 引入 PSR-3 日誌記錄關鍵節點(請求報文、響應報文、耗時),便於排查糾紛。
  • 支付狀態機:待支付 → 支付中 → 成功/失敗/超時關閉,避免臟數據。
  • 對接交行新版 API(如 REST/JSON 格式)時,可在同一插件中通過版本號路由,平滑遷移。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved