PHP后端银联支付及退款实例代码
近期遇到银联支付以及相关退款(此文仅以手机控件支付作为前提)操作,下面会依次写出期间遇到的问题以及基本流程,在此之前通过官方的一张图片了解一个支付中,对于后端人员的我们需要做到的一些事情。
由此图可以看出,后端在此负责1、平台订单生成;2、银联全渠道平台订单推送;3、返回tn码给前端进行支付;4、处理前台通知以及全渠道平台的异步通知。
此间难点有三,订单推送、异步通知处理、订单状态查询。
通过官方的邮件说明下载相关的包并放入后端php代码中,(支付控件去下载你看到的估计只有IOS,安卓版的SDK,对于后端来说,随便下载一个即可,PHP的代码在里面都有放置);然后仔细阅读SDK中的readme.txt文件,此后进行以下步骤:
一、相关参数配置
对接过程中使用在sdk的assets文件夹中测试环境配置文件及证书,放置到sdk文件夹中,并配置/sdk/SDKconfig.php文件已正确的读取acp_sdk.ini配置文件。
在acp_sdk.ini文件中配置好acpsdk.signCert.path、acpsdk.encryptCert.path、acpsdk.rootCert.path、acpsdk.middleCert.path四个文件的绝对地址(自定义文件路径即可)。
因项目开发过程中会出现系统不同或项目地址不同导致的证书绝对地址等错误,尤其在实际生产环境中,极易出现项目部署文件地址不同,不可能在开发后每次更新都要更换证书地址,在此修改了一下SDK中的SDKconfig.php已兼容不同文件地址较长,这里还请点击展开查看
<?php
namespace com\unionpay\acp\sdk;;
include_once 'log.class.php';
include_once 'common.php';
class SDKConfig {
private static $_config = null;
public static function getSDKConfig(){
if (SDKConfig::$_config == null ) {
SDKConfig::$_config = new SDKConfig();
}
return SDKConfig::$_config;
}
private $frontTransUrl;
private $backTransUrl;
private $singleQueryUrl;
private $batchTransUrl;
private $fileTransUrl;
private $appTransUrl;
private $cardTransUrl;
private $jfFrontTransUrl;
private $jfBackTransUrl;
private $jfSingleQueryUrl;
private $jfCardTransUrl;
private $jfAppTransUrl;
private $qrcBackTransUrl;
private $qrcB2cIssBackTransUrl;
private $qrcB2cMerBackTransUrl;
private $signMethod;
private $version;
private $ifValidateCNName;
private $ifValidateRemoteCert;
private $signCertPath;
private $signCertPwd;
private $validateCertDir;
private $encryptCertPath;
private $rootCertPath;
private $middleCertPath;
private $frontUrl;
private $backUrl;
private $secureKey;
private $logFilePath;
private $logLevel;
function __construct(){
//如果想把acp_sdk.ini挪到其他路径的话,请修改下面这行指定绝对路径。
$configFilePath = dirname(__FILE__) . "/acp_sdk.ini";
$certsFilePath = dirname(dirname(__FILE__)) . "/certs/";
if(!file_exists($configFilePath)){
$logger = LogUtil::getLogger();
$logger->LogError("配置文件加载失败,文件路径:[" . $configFilePath . "].请检查启动php的用户是否有读权限。");
return;
}
$ini_array = parse_ini_file($configFilePath, true);
$sdk_array = $ini_array["acpsdk"];
$this->frontTransUrl = array_key_exists("acpsdk.frontTransUrl", $sdk_array)?$sdk_array["acpsdk.frontTransUrl"] : null;
$this->backTransUrl = array_key_exists("acpsdk.backTransUrl", $sdk_array)?$sdk_array["acpsdk.backTransUrl"] : null;
$this->singleQueryUrl = array_key_exists("acpsdk.singleQueryUrl", $sdk_array)?$sdk_array["acpsdk.singleQueryUrl"] : null;
$this->batchTransUrl = array_key_exists("acpsdk.batchTransUrl", $sdk_array)?$sdk_array["acpsdk.batchTransUrl"] : null;
$this->fileTransUrl = array_key_exists("acpsdk.fileTransUrl", $sdk_array)?$sdk_array["acpsdk.fileTransUrl"] : null;
$this->appTransUrl = array_key_exists("acpsdk.appTransUrl", $sdk_array)?$sdk_array["acpsdk.appTransUrl"] : null;
$this->cardTransUrl = array_key_exists("acpsdk.cardTransUrl", $sdk_array)?$sdk_array["acpsdk.cardTransUrl"] : null;
$this->jfFrontTransUrl = array_key_exists("acpsdk.jfFrontTransUrl", $sdk_array)?$sdk_array["acpsdk.jfFrontTransUrl"] : null;
$this->jfBackTransUrl = array_key_exists("acpsdk.jfBackTransUrl", $sdk_array)?$sdk_array["acpsdk.jfBackTransUrl"] : null;
$this->jfSingleQueryUrl = array_key_exists("acpsdk.jfSingleQueryUrl", $sdk_array)?$sdk_array["acpsdk.jfSingleQueryUrl"] : null;
$this->jfCardTransUrl = array_key_exists("acpsdk.jfCardTransUrl", $sdk_array)?$sdk_array["acpsdk.jfCardTransUrl"] : null;
$this->jfAppTransUrl = array_key_exists("acpsdk.jfAppTransUrl", $sdk_array)?$sdk_array["acpsdk.jfAppTransUrl"] : null;
$this->qrcBackTransUrl = array_key_exists("acpsdk.qrcBackTransUrl", $sdk_array)?$sdk_array["acpsdk.qrcBackTransUrl"] : null;
$this->qrcB2cIssBackTransUrl = array_key_exists("acpsdk.qrcB2cIssBackTransUrl", $sdk_array)?$sdk_array["acpsdk.qrcB2cIssBackTransUrl"] : null;
$this->qrcB2cMerBackTransUrl = array_key_exists("acpsdk.qrcB2cMerBackTransUrl", $sdk_array)?$sdk_array["acpsdk.qrcB2cMerBackTransUrl"] : null;
$this->signMethod = array_key_exists("acpsdk.signMethod", $sdk_array)?$sdk_array["acpsdk.signMethod"] : null;
$this->version = array_key_exists("acpsdk.version", $sdk_array)?$sdk_array["acpsdk.version"] : null;
$this->ifValidateCNName = array_key_exists("acpsdk.ifValidateCNName", $sdk_array)?$sdk_array["acpsdk.ifValidateCNName"] : "true";
$this->ifValidateRemoteCert = array_key_exists("acpsdk.ifValidateRemoteCert", $sdk_array)?$sdk_array["acpsdk.ifValidateRemoteCert"] : "false";
$this->signCertPath = $certsFilePath . (array_key_exists("acpsdk.signCert.path", $sdk_array)?$sdk_array["acpsdk.signCert.path"]: null);
$this->signCertPwd = array_key_exists("acpsdk.signCert.pwd", $sdk_array)?$sdk_array["acpsdk.signCert.pwd"]: null;
$this->validateCertDir = array_key_exists("acpsdk.validateCert.dir", $sdk_array)? $sdk_array["acpsdk.validateCert.dir"]: null;
$this->encryptCertPath = $certsFilePath . (array_key_exists("acpsdk.encryptCert.path", $sdk_array)? $sdk_array["acpsdk.encryptCert.path"]: null);
$this->rootCertPath = $certsFilePath . (array_key_exists("acpsdk.rootCert.path", $sdk_array)? $sdk_array["acpsdk.rootCert.path"]: null);
$this->middleCertPath = $certsFilePath . (array_key_exists("acpsdk.middleCert.path", $sdk_array)?$sdk_array["acpsdk.middleCert.path"]: null);
$this->frontUrl = array_key_exists("acpsdk.frontUrl", $sdk_array)?$sdk_array["acpsdk.frontUrl"]: null;
$this->backUrl = array_key_exists("acpsdk.backUrl", $sdk_array)?$sdk_array["acpsdk.backUrl"]: null;
$this->secureKey = array_key_exists("acpsdk.secureKey", $sdk_array)?$sdk_array["acpsdk.secureKey"]: null;
$this->logFilePath = array_key_exists("acpsdk.log.file.path", $sdk_array)?$sdk_array["acpsdk.log.file.path"]: null;
$this->logLevel = array_key_exists("acpsdk.log.level", $sdk_array)?$sdk_array["acpsdk.log.level"]: null;
}
public function __get($property_name)
{
if(isset($this->$property_name))
{
return($this->$property_name);
}
else
{
return(NULL);
}
}
<strong>}
</strong>
二、全渠道商品订单推送
相关代码请点击查看
use com\unionpay\acp\sdk\AcpService;
use com\unionpay\acp\sdk\LogUtil;
use com\unionpay\acp\sdk\SDKConfig;
/**
* 银联支付下单
*
* @param $orders
* @param $orders_type
* @return array
*/
public function unionPay($orders, $orders_type = 0)
{
include_once dirname(dirname(dirname(__FILE__))) . '/Model/unionpay-sdk/sdk/acp_service.php';
$config = new SDKConfig();
$AcpService = new AcpService();
$log = LogUtil::getLogger();
$time = date('YmdHis', time());
$params = array(
//以下信息非特殊情况不需要改动
'version' => $config->getSDKConfig()->version, //版本号
'encoding' => 'utf-8', //编码方式
'txnType' => '01', //交易类型
'txnSubType' => '01', //交易子类
'bizType' => '000201', //业务类型
'frontUrl' => $config->getSDKConfig()->frontUrl, //前台通知地址
'backUrl' => $this->getURL('api_pay_unionpay_call_back'), //后台通知地址
'signMethod' => $config->getSDKConfig()->signMethod, //签名方法
'channelType' => '08', //渠道类型,07-PC,08-手机
'accessType' => '0', //接入类型
'currencyCode' => '156', //交易币种,境内商户固定156
//TODO 以下信息需要填写
'merId' => $this->getParameter('mer_id'), //商户代码,请改自己的测试商户号
'orderId' => $orders["order_no"], //商户订单号,8-32位数字字母,不能含“-”或“_”
'txnTime' => $time, //订单发送时间,格式为YYYYMMDDhhmmss,取北京时间
'txnAmt' => $orders['total_price'] * 100, //交易金额,单位分
);
$AcpService->sign ( $params ); // 签名
$url = $config->getSDKConfig()->appTransUrl;
$result_arr = $AcpService->post ($params, $url);
if(count($result_arr)<=0) { //没收到200应答的情况 $log->LogInfo('没收到200应答的情况');
}
// $this->printResult ($url, $params, $result_arr ); //页面打印请求应答数据
if (!$AcpService->validate ($result_arr) ){
$log->LogInfo('应答报文验签失败');
}
if ($result_arr["respCode"] == "00"){
//成功
return array('txn_time'=>$time, 'tn'=>$result_arr["tn"]);
// echo "后续请将此tn传给手机开发,由他们用此tn调起控件后完成支付。
\n";
// echo "手机端demo默认从仿真获取tn,仿真只返回一个tn,如不想修改手机和后台间的通讯方式,【此页面请修改代码为只输出tn】。
\n";
} else {
//其他应答码做以失败处理
return array('txn_time'=>$time, 'tn'=>0);
//echo "失败:" . $result_arr["respMsg"] . "。
\n";
}
}
在此注意txnTime格式不要传错,测试环境下应该不会出现什么问题,将得到的tn返回APP进行支付即可
三、异步通知处理以及订单交易状态查询
这一步主要作用为处理银联交易成功信息,并尽可能避免出现回调未处理导致问题。
先说异步通知处理,此步骤为订单状态修改的主要依据。无实际难点,保证相关参数无问题即可
/**
* 银联回调
*
* @param Request $request
* @return array|Response
*/
public function unionPayCallBackAction(Request $request)
{
if ($request->get('type') == 1){//前台通知-进行订单状态查询
$query = $this->unionPayQuery($request, array(), 1);
return new JsonResponse($query);
}
require_once dirname(dirname(dirname(__FILE__))) . "/Model/unionpay-sdk/sdk/acp_service.php";
$log = LogUtil::getLogger();
$AcpService = new AcpService();
if ($request->request->has('signature') && $AcpService->validate($_POST)) {
$order_no = $request->request->get('orderId');
$respCode = $request->request->get('respCode');
$total = $request->request->get('txnAmt'); // 交易金额
if ($respCode === '00' || $respCode === 'A6') {
$trade_no = $request->request->get('origQryId')?:'UN' . date('YmdHis', time()) . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
$this->dispose($order_no, $trade_no, 4);//订单交易处理-请根据实际情况自行编写
}
} else {
if (!$request->request->has('signature')) {
$log->LogInfo('签名为空');
} else {
$log->LogInfo('验签失败');
}
}
exit;
}
订单交易状态查询
do{//循环查询,直到获取到退款订单的queryID
sleep($number * 2);
$query = $this->unionPayQuery('', $orders);
$number += 1;
}while($query['errorCode'] != 0 || empty($query['result_arr']["queryId"]));
public function unionPayQuery($request, $orders)
{
require_once dirname(dirname(dirname(__FILE__))) . "/Model/unionpay-sdk/sdk/acp_service.php";
$config = new SDKConfig();
$AcpService = new AcpService();
$log = LogUtil::getLogger();
$params = array(
//以下信息非特殊情况不需要改动
'version' => $config->getSDKConfig()->version, //版本号
'encoding' => 'utf-8', //编码方式
'signMethod' => $config->getSDKConfig()->signMethod, //签名方法
'txnType' => '00', //交易类型
'txnSubType' => '00', //交易子类
'bizType' => '000000', //业务类型
'accessType' => '0', //接入类型
'channelType' => '07', //渠道类型
//TODO 以下信息需要填写
'orderId' => $orders['order_no'], //请修改被查询的交易的订单号,8-32位数字字母,不能含“-”或“_”
'merId' => $this->getParameter('mer_id'), //商户代码,请改自己的测试商户号
'txnTime' => date('YmdHis', time()), //请修改被查询的交易的订单发送时间,格式为YYYYMMDDhhmmss
);
$AcpService->sign ( $params ); // 签名
$url = $config->getSDKConfig()->singleQueryUrl;
$result_arr = $AcpService->post ( $params, $url);
if(count($result_arr)<=0) { //没收到200应答的情况 $log->LogInfo('没收到200应答的情况');
}
if (!$AcpService->validate ($result_arr) ){
$log->LogInfo('应答报文验签失败');
}
if ($result_arr["respCode"] == "00"){
if ($result_arr["origRespCode"] == "00"){
//交易成功
$trade_no = 'UN' . date('YmdHis', time()) . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
$this->dispose($orders['order_no'], $trade_no, 4);
$result = array('errorCode'=>0, 'message'=>'交易成功', 'result_arr'=>$result_arr);
} else if ($result_arr["origRespCode"] == "03"
|| $result_arr["origRespCode"] == "04"
|| $result_arr["origRespCode"] == "05"){
//后续需发起交易状态查询交易确定交易状态
$result = array('errorCode'=>2, 'message'=>'交易处理中', 'result_arr'=>$result_arr);
} else {
//其他应答码做以失败处理
echo "交易失败:" . $result_arr["origRespMsg"] . "。
\n";
$result = array('errorCode'=>1, 'message'=>"交易失败:" . $result_arr["origRespMsg"] . ".", 'result_arr'=>$result_arr);
}
} else if ($result_arr["respCode"] == "03"
|| $result_arr["respCode"] == "04"
|| $result_arr["respCode"] == "05" ){
//后续需发起交易状态查询交易确定交易状态
$result = array('errorCode'=>2, 'message'=>"处理超时,请稍后查询.", 'result_arr'=>$result_arr);
} else {
//其他应答码做以失败处理
$result = array('errorCode'=>1, 'message'=>"失败:" . $result_arr["respMsg"] . ".", 'result_arr'=>$result_arr);
}
return $result;
}
到此为止,若是项目没有订单线上退款就完成了。
订单退款相关
public function refundUnionPay($orders)
{
require_once(dirname(dirname(__FILE__)) . "/Model/unionpay-sdk/sdk/acp_service.php");
set_time_limit(100);
$config = new SDKConfig();
$AcpService = new AcpService();
$log = LogUtil::getLogger();
$number = 0;
do{//循环查询,直到获取到退款订单的queryID
sleep($number * 2);
$query = $this->unionPayQuery('', $orders);
$number += 1;
}while($query['errorCode'] != 0 || empty($query['result_arr']["queryId"]));
if ($query['errorCode'] != 0) {
return array('errorCode'=>1, 'message'=>'订单未成交,无法退款');
}
$params = array(
//以下信息非特殊情况不需要改动
'version' => $config->getSDKConfig()->version, //版本号
'encoding' => 'utf-8', //编码方式
'signMethod' => $config->getSDKConfig()->signMethod, //签名方法
'txnType' => '04', //交易类型
'txnSubType' => '00', //交易子类
'bizType' => '000201', //业务类型
'accessType' => '0', //接入类型
'channelType' => '07', //渠道类型
'backUrl' => $config->getSDKConfig()->backUrl, //后台通知地址
//TODO 以下信息需要填写
'orderId' => "T" . $orders['order_no'], //商户订单号,8-32位数字字母,不能含“-”或“_”,可以自行定制规则,重新产生-此处为在退款订单前拼接 T
'merId' => $this->getParameter('mer_id'), //商户代码,请改成自己的商户号
'origQryId' => $query['result_arr']["queryId"], //原消费的queryId,可以从查询接口或者通知接口中获取
'txnTime' => date('YmdHis', time()), //订单发送时间,格式为YYYYMMDDhhmmss,重新产生,不同于原消费
'txnAmt' => $orders['total_price'] * 100, //交易金额,退货总金额需要小于等于原消费
);
$AcpService->sign ( $params ); // 签名
$url = $config->getSDKConfig()->backTransUrl;
$result_arr = $AcpService->post ( $params, $url);
if(count($result_arr)<=0) { //没收到200应答的情况 return array('errorCode'=>1, 'message'=>"没收到应答.");
}
if (!$AcpService->validate ($result_arr) ){
return array('errorCode'=>1, 'message'=>"应答报文验签失败.");
}
if ($result_arr["respCode"] == "00"){
//交易已受理,等待接收后台通知更新订单状态,如果通知长时间未收到也可发起交易状态查询
return array('errorCode'=>0, 'message'=>"受理成功.");
} else if ($result_arr["respCode"] == "03"
|| $result_arr["respCode"] == "04"
|| $result_arr["respCode"] == "05" ){
//后续需发起交易状态查询交易确定交易状态
return array('errorCode'=>1, 'message'=>"处理超时,请稍微查询.");
} else {
//其他应答码做以失败处理
return array('errorCode'=>1, 'message'=>"失败:" . $result_arr["respMsg"] . ".");
}
}
依据返回状态值进行相关操作即可,实际逻辑代码请自行实现