PHP后端银联支付及退款实例代码


Posted in PHP onJune 23, 2017

声明:此文以当前银联官方最新SDK(2016-08-09 5.1.0版)进行说明,若出现包不相同的情况请检查是否是此版本

近期遇到银联支付以及相关退款(此文仅以手机控件支付作为前提)操作,下面会依次写出期间遇到的问题以及基本流程,在此之前通过官方的一张图片了解一个支付中,对于后端人员的我们需要做到的一些事情

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);
    }
  }
 
}

二、全渠道商品订单推送

相关代码请点击查看

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"] . ".");
    }
  }

依据返回状态值进行相关操作即可,实际逻辑代码请自行实现

切换生产环境

项目关系暂无法进行-后续补充

未完待续。。。。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
网站加速 PHP 缓冲的免费实现方法
Oct 09 PHP
ZF等常用php框架中存在的问题
Jan 10 PHP
php中将html中的br换行符转换为文本输入中的换行符
Mar 26 PHP
PHP+MYSQL中文乱码问题
Jul 01 PHP
基于PHP如何把汉字转化为拼音
Dec 11 PHP
使用PHP实现微信摇一摇周边红包
Jan 04 PHP
YII动态模型(动态表名)支持分析
Mar 29 PHP
php使用正则表达式获取字符串中的URL
Dec 29 PHP
PHP实现根据数组的值进行分组的方法
Apr 20 PHP
创建无限极分类树型结构的简单方法
Jun 20 PHP
php的常量和变量实例详解
Jun 27 PHP
php 二维数组快速排序算法的实现代码
Oct 17 PHP
PHP单例模式简单用法示例
Jun 23 #PHP
php处理静态页面:页面设置缓存时间实例
Jun 22 #PHP
PHP使用redis消息队列发布微博的方法示例
Jun 22 #PHP
用PHP去掉文件头的Unicode签名(BOM)方法
Jun 22 #PHP
PHP简单装饰器模式实现与用法示例
Jun 22 #PHP
PHP实现的简单适配器模式示例
Jun 22 #PHP
php smtp实现发送邮件功能
Jun 22 #PHP
You might like
PHP 数组排序方法总结 推荐收藏
2010/06/30 PHP
php排序算法(冒泡排序,快速排序)
2012/10/09 PHP
php实现统计邮件大小的方法
2013/08/06 PHP
浅析php单例模式
2014/11/25 PHP
PHP输出多个元素的排列或组合的方法
2017/03/14 PHP
PHP实现Markdown文章上传到七牛图床的实例内容
2020/02/11 PHP
在JS中最常看到切最容易迷惑的语法(转)
2010/10/29 Javascript
jQuery插件slicebox实现3D动画图片轮播切换特效
2015/04/12 Javascript
javascript禁止超链接跳转的方法
2016/02/02 Javascript
JavaScript动态添加事件之事件委托
2016/07/12 Javascript
JS中script标签defer和async属性的区别详解
2016/08/12 Javascript
js生成随机数方法和实例
2017/01/17 Javascript
vue实现todolist单页面应用
2017/04/11 Javascript
jQuery length 和 size()区别总结
2018/04/26 jQuery
ES6 Proxy实现Vue的变化检测问题
2019/06/11 Javascript
Jquery+AJAX实现无刷新上传并重命名文件操作示例【PHP后台接收】
2020/05/29 jQuery
vue将data恢复到初始状态 &amp;&amp; 重新渲染组件实例
2020/09/04 Javascript
[02:52]DOTA2新手基础教程 米波
2014/01/21 DOTA
Python脚本判断 Linux 是否运行在虚拟机上
2015/04/25 Python
Python实现模拟登录及表单提交的方法
2015/07/25 Python
详解使用Python处理文件目录的相关方法
2015/10/16 Python
11个Python3字典内置方法大全与示例汇总
2019/05/13 Python
python3实现斐波那契数列(4种方法)
2019/07/15 Python
python中下标和切片的使用方法解析
2019/08/27 Python
余弦相似性计算及python代码实现过程解析
2019/09/18 Python
Python类如何定义私有变量
2020/02/03 Python
如何在Python对Excel进行读取
2020/06/04 Python
python获得命令行输入的参数的两种方式
2020/11/02 Python
CSS3 实现的火焰动画
2020/12/07 HTML / CSS
StubHub巴西:购买和出售您的门票
2016/07/22 全球购物
美国网上鞋子零售商:Dr. Scholl’s Shoes
2017/11/17 全球购物
公司中层干部的自我评价分享
2014/03/01 职场文书
国土资源局开展党的群众路线教育实践活动整改措施
2014/09/26 职场文书
2015年六一儿童节演讲稿
2015/03/19 职场文书
采购内勤岗位职责
2015/04/13 职场文书
Angular CLI发布路径的配置项浅析
2021/03/29 Javascript