ECSHOP 官方未內置交通銀行(BankComm)支付插件。項目需對接交行 B2C 網銀支付接口(版本 1.0.0.0),採用雙向 Socket 通訊 + XML 報文 + 數字簽名模式。本文記錄插件核心邏輯、關鍵代碼與踩坑點,供同類二次開發參考。
orderUrl 和 signMsg → 生成提交表單頁面導向交行支付頁。notifyMsg 到商戶 respond.php → 商戶再次發送驗證報文至前置機 → 解析結果 → 更新訂單狀態。includes/modules/payment/bankcomm.php # 支付插件主類
languages/zh_cn/payment/bankcomm.php # 語言包
respond.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 = '';
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;
}
}
?>
<?php
global $_LANG;
$_LANG['bankcomm'] = '交通銀行在線支付';
$_LANG['bankcomm_desc'] = '支持交通銀行借記卡/信用卡網銀支付';
$_LANG['pay_button'] = '交通銀行支付';
?>
<?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';
}
?>
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));
$source 時必須完全按照接口文檔規定的字段順序,否則簽名驗證失敗。100.00。respond() 可能被多次調用(網絡重試),內部 order_paid() 已有判重邏輯,但建議自行記錄處理日誌防重。