银联在线支付商户UPMP接口的使用和说明

  •   
  • 24736
  • php
  • 7
  • dodobook
  • 2014/09/13

现在互联网支付日渐流行和成熟。除了支付宝支付外,传统的银联支付也走进很多商家的视野。毕竟现在一些传统用户还是以银联银行卡为主。银联的商家的API接口也成为网站开发的必备知识和技能。

银联开发的主要步骤是,先申请测试账户。使用测试账户进行开发和测试。提交测试报告。通过审核之后,分配给商家入户参数。根据如何参数小范围的修改和测试后,即可投入使用。

相关的文档可以参阅。http://202.101.25.178:8080/sim/docs/

这个接口文档还是比较完备的,相信很多技术人员稍加摸索,就能实现了。

我的是使用Yii的框架,我为了便捷的操作。把文档地址上面的目录结构多余的地方去除了。全部在一个文件夹下面(Upmp)。目录结构为

Upmp
----upmp_config.php
----upmp_core.php
----UpmpService.php
----notify_url.php

直接上代码:upmp_config.php

<?php

/**
 * 类名:配置类
 * 功能:配置类
 * 类属性:公共类
 * 版本:1.0
 * 日期:2012-10-11
 * 作者:中国银联UPMP团队
 * 版权:中国银联
 * 说明:以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己的需要,按照技术文档编写,并非一定要使用该代码。该代码仅供参考。
 * */

class upmp_config
{
	
    static $timezone                = "Asia/Shanghai"; //时区
    
    static $version                 = "2.1.4"; // 版本号(测试的时候用2.1.4版本)
    static $charset                 = "UTF-8"; // 字符编码
    static $sign_method             = "MD5"; // 签名方法,目前仅支持MD5
    
    static $mer_id                  = "8800000000*****"; // 商户号
    static $security_key            = "1nHFngmByQgZ5N71OrfO7yS*******"; // 商户密钥
    static $mer_back_end_url        = "http://pay.dodobook.net/api/upmp/successByUpmp"; // 后台通知地址
    static $mer_front_end_url       = "http://pay.dodobook.net/api/upmp/successByUpmp"; // 前台通知地址

    static $upmp_trade_url          = "http://202.101.25.178:8080/gateway/merchant/trade";
    static $upmp_query_url          = "http://202.101.25.178:8080/gateway/merchant/query";  
    
    const VERIFY_HTTPS_CERT         = false;
    
    const RESPONSE_CODE_SUCCESS     = "00"; // 成功应答码
    const SIGNATURE                 = "signature"; // 签名
    const SIGN_METHOD               = "signMethod"; // 签名方法
    const RESPONSE_CODE             = "respCode"; // 应答码
    const RESPONSE_MSG              = "respMsg"; // 应答信息
    
    const QSTRING_SPLIT             = "&"; // &
    const QSTRING_EQUAL             = "="; // =
    
}

?>

upmp_core.php

<?php
/**
 * 类名:交易服务类
 * 功能:接口公用函数类
 * 版本:1.0
 * 日期:2012-10-11
 * 作者:中国银联UPMP团队
 * 版权:中国银联
 * 说明:以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己的需要,按照技术文档编写,并非一定要使用该代码。该代码仅供参考。
 * */

require_once("upmp_config.php");

/**
 * 除去请求要素中的空值和签名参数
 * @param para 请求要素
 * @return 去掉空值与签名参数后的请求要素
 */
function paraFilter($para) {
	$result = array ();
	while ( list ( $key, $value ) = each ( $para ) ) {
		if ($key == upmp_config::SIGNATURE || $key == upmp_config::SIGN_METHOD || $value == "") {
			continue;
		} else {
			$result [$key] = $para [$key];
		}
	}
	return $result;
}

/**
 * 生成签名
 * @param req 需要签名的要素
 * @return 签名结果字符串
 */
function buildSignature($req) {
	$prestr = createLinkstring($req, true, false);
	$prestr = $prestr.upmp_config::QSTRING_SPLIT.md5(upmp_config::$security_key);
	return md5($prestr);
}

/**
 * 把请求要素按照“参数=参数值”的模式用“&”字符拼接成字符串
 * @param para 请求要素
 * @param sort 是否需要根据key值作升序排列
 * @param encode 是否需要URL编码
 * @return 拼接成的字符串
 */
function createLinkString($para, $sort, $encode) {
	$linkString  = "";
	if ($sort){
		$para = argSort($para);
	}
	while (list ($key, $value) = each ($para)) {
		if ($encode){
			$value = urlencode($value);
		}
		$linkString.=$key.upmp_config::QSTRING_EQUAL.$value.upmp_config::QSTRING_SPLIT;
	}
	//去掉最后一个&字符
	$linkString = substr($linkString,0,count($linkString)-2);
	
	return $linkString;
}

/**
 * 对数组排序
 * @param $para 排序前的数组
 * return 排序后的数组
 */
function argSort($para) {
	ksort($para);
	reset($para);
	return $para;
}

/*
 * curl_call
*
* @url:  string, curl url to call, may have query string like ?a=b
* @content: array(key => value), data for post
*
* return param:
*	mixed:
*	  false: error happened
*	  string: curl return data
*
*/
function post($url, $content = null)
{
	if (function_exists("curl_init")) {
		$curl = curl_init();

		if (is_array($content)) {
			$data = http_build_query($content);
		}

		curl_setopt($curl, CURLOPT_POST, 1);
		curl_setopt($curl, CURLOPT_POSTFIELDS, $content);
		curl_setopt($curl, CURLOPT_URL, $url);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($curl, CURLOPT_HEADER, false);
		curl_setopt($curl, CURLOPT_TIMEOUT, 60); //seconds
		
		// https verify
		curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, upmp_config::VERIFY_HTTPS_CERT);
		curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, upmp_config::VERIFY_HTTPS_CERT);

		$ret_data = curl_exec($curl);

		if (curl_errno($curl)) {
			printf("curl call error(%s): %s\n", curl_errno($curl), curl_error($curl));
			curl_close($curl);
			return false;
		}
		else {
			curl_close($curl);
			return $ret_data;
		}
	} else {
		throw new Exception("[PHP] curl module is required");
	}
}

?>

UpmpService.php

<?php
/**
 * 类名:接口处理核心类
 * 功能:组转报文请求,发送报文,解析应答报文
 * 版本:1.0
 * 日期:2012-10-11
 * 作者:中国银联UPMP团队
 * 版权:中国银联
 * 说明:以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己的需要,按照技术文档编写,并非一定要使用该代码。该代码仅供参考。
 */

require_once("upmp_core.php");

if (function_exists("date_default_timezone_set")) {
	date_default_timezone_set(upmp_config::$timezone);
}

class UpmpService {
    
    /**
     * 交易接口处理
     * @param req 请求要素
     * @param resp 应答要素
     * @return 是否成功
     */
    static function trade($req, &$resp) {
    	$nvp = self::buildReq($req);
    //    file_put_contents('charge_req.txt', $nvp,FILE_APPEND); //写文件记录,用于提交请求的报文
    	$respString = post(upmp_config::$upmp_trade_url, $nvp);
    //    file_put_contents('aa.txt', $respString);         //写文件记录用于报文信息
    	return self::verifyResponse($respString, $resp);
    }
    
	/**
	 * 交易查询处理
	 * @param req 请求要素
	 * @param resp 应答要素
	 * @return 是否成功
	 */
    static function query($req, &$resp) {
    	$nvp = self::buildReq($req);
   //     file_put_contents('charge_search.txt', $nvp,FILE_APPEND);  //写文件记录,用于提交请求的报文 
    	$respString = post(upmp_config::$upmp_query_url, $nvp);
    //    file_put_contents('bb.txt', $respString);           //写文件记录用于报文信息
    	return self::verifyResponse($respString, $resp);
    }
    
    /**
     * 拼接请求字符串
     * @param req 请求要素
     * @return 请求字符串
     */
    static function buildReq($req) {
    	//除去待签名参数数组中的空值和签名参数
    	$filteredReq = paraFilter($req);
    	// 生成签名结果
    	$signature = buildSignature($filteredReq);
    	
    	// 签名结果与签名方式加入请求
    	$filteredReq[upmp_config::SIGNATURE] = $signature;
    	$filteredReq[upmp_config::SIGN_METHOD] = upmp_config::$sign_method;
    	
    	return createLinkstring($filteredReq, false, true);
    }
    
    /**
     * 拼接保留域
     * @param req 请求要素
     * @return 保留域
     */
    static function buildReserved($req) {
    	$prestr = "{".createLinkstring($req, true, true)."}";
    	return $prestr;
    }
    
    /**
     * 应答解析
     * @param respString 应答报文
     * @param resp 应答要素
     * @return 应答是否成功
     */
    static function verifyResponse($respString, &$resp) {
    	if  ($respString != ""){
    		parse_str($respString, $para);
    		
    		$signIsValid = self::verifySignature($para);

    		$resp = $para;
    		if ($signIsValid) {
    			return true;
    		}else {
    			return false;
    		}
    	}
    }
    
    /**
     * 异步通知消息验证
     * @param para 异步通知消息
     * @return 验证结果
     */
    static function verifySignature($para) {
    	$respSignature = $para[upmp_config::SIGNATURE];
    	// 除去数组中的空值和签名参数
    	$filteredReq = paraFilter($para);
    	$signature = buildSignature($filteredReq);
    	if ("" != $respSignature && $respSignature==$signature) {
    		return true;
    	}else {
    		return false;
    	}
    }
	
}
?>

修改成分配给你的参数。至此基本上这个包已经完整了。上面还有一些写文件的操作。用于填写报文。

调用的方法和成功的回调响应的接口


//会员充值到账户余额--使用银联--生成订单(简易版本)
public function actionChargeByUpmp() {
	$request = Yii::app()->request;
	Yii::import('application.extensions.Upmp.*');        //引入Upmp
	$member_id = $request->getParam('mid');				//会员ID
	$number =  $request->getParam('number');			//会员充值的金额
	//加入对会员的安全状态各方面的检测和处理.......

	$transaction = Yii::app()->db->beginTransaction();		//使用事务能保证尽可能少的产生脏数据
	try{
		//先本地生成订单,生成订单ID
		$billList = new MemberBillList;
		$billList->member_id = $member_id;
		$billList->number = $number;			//充值金额
		$billList->in_out = '1';				//会员支出
		$billList->in_out_type = '2';			//会员个人余额充值
		$billList->add_time = time();
		$billList->status = '2';				//账单状态未完成
		$billList->remark = '会员银联充值';		//账单状态未完成
		$billList->isNewRecord = true;
		if(!$billList->save()){
			throw new Exception('生成账单记录失败,'.$this->getModelFirstError($billList), 11);
		}

		//银联的这个账单号的长度是(8-40),我在文档上只看到了不大于40,没看到最小8.(后来询问才知道)
		$orderNumber = date('Ymd').'MCHA'.$billList->id;	//本地的账单ID,经过组装后用于提交给银联
	
		//去服务器上生成流水号
		$notify_url = 'http://pay.dodobook.net/api/upmp/successByChargeUpmp';	//通知的地址(成功之后每隔几分钟向该地址发送成功的通知)
		$bankRt = $this->actionCreateBill($orderNumber,$number,$notify_url);	//根据提交的信息去请求并生成流水号

		if($bankRt['respCode'] != '00'){		//判断返回的状态
			throw new Exception('银行接口返回失败,'.$bankRt['respMsg'], 10);
		}

		if(!$bankRt['tn']){			//判断流水号
			throw new Exception('银行接口流水号未返回', 11);
		}

		//更新刚才的账单的信息,增加刚才账单和流水号等的对应(完整记录)
		$bank_order_id = $bankRt['tn'];
		$newBillList = MemberBillList::model()->findByPk($billList->id);
		$newBillList->bank_tn_id = $bank_order_id;		//银行流水号
		$newBillList->bank_order_id = $orderNumber;		//银行系统记录的账单号

		if(!$newBillList->save()){
			throw new Exception('更新流水号失败', 12);
		}

		$transaction->commit();
		$json['id'] = date("YmdHis").'_'.$billList->id;		//账单的ID
		$json['bank_order_id'] = $bank_order_id;		//银行流水号

	//	$this->_end_api('0','账单记录生成成功,账单状态为未完成.',$json);
	}catch(Exception $e){
		$transaction->rollback();
	//	$this->_end_api($e->getCode(),'生成账单记录失败',$e->getMessage());
	}
}

//会员充值到账户余额--使用银联--回调方法
public function actionSuccessByChargeUpmp() {
	$request = Yii::app()->request;
	Yii::import('application.extensions.Upmp.*');        //引入Upmp

	测试的时候把返回过来的回调的信息先写文本日志记录
	$str = '';
	foreach ($_POST as $k => $v) {
		$str .= $k .'==='.$v.'<hr>';
	}

	file_put_contents('charge_upmp.txt', '交易时间'.date('Y-m-d H:i:s').$str,FILE_APPEND);
	if($_POST['transStatus'] == '00'){
		echo 'success';
	}
	exit();

/*	//正式环境下的账单的处理
	if (UpmpService::verifySignature($_POST)){// 服务器签名验证成功
		//请在这里加上商户的业务逻辑程序代码
		//获取通知返回参数,可参考接口文档中通知参数列表(以下仅供参考)
		$transStatus = $_POST['transStatus'];// 交易状态
		if ("" != $transStatus && "00"==$transStatus){
			// 交易处理成功
			$qn = $_POST['qn']; 					//查询流水号
			$orderNumber = $_POST['orderNumber'];	//商户订单号
			$total_fee = $_POST['settleAmount'] / 100;	//清算金额

			$billList = MemberBillList::model()->find("bank_order_id = '{$orderNumber}'");
			$bill_list_id = $billList->id;					 //账单记录ID
			$memBillList = MemberBillList::model()->findByPk($bill_list_id);
			if($memBillList->status == '1'){				//如果已处理过
				echo 'success';
				$this->_end_api('0','Sucess','Sucess');		//处理成功
			}
			
			$transaction = Yii::app()->db->beginTransaction();
			try{
				//bill_list_id 
				//total_fee     银联支付的金额
				//更新会员账单记录表 member_bill_list
				$memBillList = MemberBillList::model()->findByPk($bill_list_id);
				$member_id = $memBillList->member_id;		//消费的会员ID
				$memBill = MemberBill::model()->find("member_id = '{$member_id}'");
				$memBillList->status = '1';					//更新状态值为已完成
				if(!$memBillList->save()){
					$this->_end_api('1','更改会员账单记录表状态失败',$this->getModelFirstError($memBillList));
				}

				//更新会员账单--.............
				
				$transaction->commit();
				echo 'success';				//成功之后告诉银联,避免银联不断的发送信息过来,会尝试24小时,5次
				$this->_end_api('0','success','success');		//处理成功
			}catch(Exception $e){
				$transaction->rollback();
				echo "fail";
				$this->_end_api('12','银联支付回调处理失败','');
			}
		}else {
			file_put_contents('charge_upmp_err.txt', '失败:交易时间'.date('Y-m-d H:i:s').$str,FILE_APPEND);
		}
		echo "success";
	}else {// 服务器签名验证失败
		echo "fail";
		$this->_end_api('1','交易失败','');
	}
	*/
}

//生成账单请求的公共方法
public function actionCreateBill($orderNumber='',$orderAmount='',$notify_url) {
	header('Content-Type:text/html;charset=utf-8');
	Yii::import('application.extensions.Upmp.*');        //引入Upmp

	//需要填入的部分
	$req['version']     		= upmp_config::$version; 	// 版本号
	$req['charset']     		= upmp_config::$charset; 	// 字符编码
	$req['transType']   		= "01"; 					// 交易类型
	$req['merId']       		= upmp_config::$mer_id; 	// 商户代码
	$req['backEndUrl']      	= $notify_url; 				// 通知URL
	$req['frontEndUrl']     	= $notify_url; 				// 前台通知URL(可选)
	$req['orderDescription']	= "订单描述";				// 订单描述(可选)
	$req['orderTime']   		= date("YmdHis"); 			// 交易开始日期时间yyyyMMddHHmmss
	$req['orderTimeout']   		= ""; 						// 订单超时时间yyyyMMddHHmmss(可选)
	$req['orderNumber'] 		= $orderNumber; 			//订单号(商户根据自己需要生成订单号)
	$req['orderAmount'] 		= intval($orderAmount * 100); // 订单金额(人民币单位为分)
	$req['orderCurrency'] 		= "156"; 					// 交易币种(可选)--人民币
	$req['reqReserved'] 		= "透传信息"; 				// 请求方保留域(可选,用于透传商户信息)

	// 保留域填充方法
	$merReserved['test']   		= "test";
	$req['merReserved']   		= UpmpService::buildReserved($merReserved); // 商户保留域(可选)

	$resp = array ();
	$validResp = UpmpService::trade($req, $resp);

	// 商户的业务逻辑
	if ($validResp){		// 服务器应答签名验证成功
		return $resp;
	}else {					// 服务器应答签名验证失败
		return $resp;
	}
}

备注说明:
1.可以完全按照银联提供的接口和example来使用,有时候会报错,大多数情况下是目录结构和命名规范导致。自己改进。
2.测试报告的报文,都是需要状态为成功的才能通过。即测试的订单推送等都是需要完成的。
3.用户发出的请求报文,可以通过写文件操作记录。收到的应答报文也可以写文件记录。根据他们的示例。
4.银联这边的客服还是很不错的,毕竟加入了一个QQ群,都有专门的人负责及时的沟通和解决。不明白的地方,放心大胆得到问。
5.文档可能有进一步的改进,请到官网去查询相关的资料。此处只是个人的一个demo的记录。
6.欢迎大家交流和指点。

先天下之忧而忧,后天下之乐而乐.经济兴邦,心怀天下,不忘报国之志。