本帖 部分为转贴 顺便吐槽一下 微信的开发者文档真是坑啊php
首先做为服务端 你要了解 整个APP 支付流程 看文档前端
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3 json
而后 下载官方给的SDK https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=11_1api
咦? 没有服务端的SDK 傻逼了吧? %$#^&腾讯 不用着急 等会我给你 - -!数组
秋高气爽,天气转凉,正是学习工做作的好时候。(~ ̄▽ ̄)~~(~ ̄▽ ̄)~安全
我是个phper最近在写微信支付(APP支付),微信给的官方文档并非很详细也没有dome之类的代码啥的(对于新手来讲比较麻烦),本人是新手之前也没写过支付,踩了好多坑,因此想写篇文章给没写过支付的新手几个建议。服务器
这首先呢你得注册个开放平台以及商户平台的帐号吧,注册完成后呢你会收到一封微信里邮件里面有你的商户号等信息,注册这俩帐号完你会拥有商户号,appid,appkey等须要的东西。微信
准备完成后咱们来看一下支付的大致流程并发
商户APP应用与微信支付主要的交互说明:app
用户在商户APP应用(移动端)中选择商品提交订单,支付方式选择微信支付。
商户APP应用(后台)收到用户支付订单,调用微信支付中的统一下单接口。
商户APP应用(后台)统一下单接口调用成功后,返回的数据中有咱们须要的prepay_id,按照签名规范从新生成一个签名,而后把这个从新生成的签名及app须要的数据返回给商户APP应用(移动端)。
商户APP应用(移动端)收到商户APP应用(后台)的数据调起微信支付,用户进行支付
商户APP应用(后台)的回调接口会收到微信发来的支付结果通知
商户APP应用(后台)查询支付结果通知
附:1,4是移动端所要作的事情,2,3,5是咱们PHP服务端后台要作的6也是,但我没作,这个根据状况而定若是须要的话就作。
步骤1由移动端完成
步骤2. 调用同一下单接口:
先要作的是流程中的第二步,调用同一下单接口。官方文档里说了请求的地址与参数,其中有一些是必填参数,有
appid
应用ID 固定值,你申请帐号时就给你了
mch_id
商户号 固定值,你申请帐号时就给你了
nonce_str
随机字符串 这个是本身写的要求不能长于32位,参见官方给的[标准][8]
sign
签名 咱们把这个签名叫作第一次签名,注意这个是个坑,得本身写了,官方只给了如何写的[标准][9]没有代码,这个就比较蛋疼了。好多人掉进这个坑里,写的签名函数不对,总是出错。但不用担忧我在文章的最后会贴出代码里面有签名函数直接调用就能够了。(注意看我写的函数使用规则)
body
商品描述 固定值 商品描述交易字段格式根据不一样的应用场景按照如下格式:APP——需传入应用市场上的APP名字-实际商品名称,每天爱消除-游戏充值。
out_trade_no
商户订单号 咱们本身定义的订单号,32个字符内、可包含字母。
total_fee
总金额 这个就是你要支付的钱数了,由前端返回。注意一下这里的货币单位是分!
spbill_create_ip
终端IP 这个用户的IP地址,写个取IP地址的函数一调用就行
notify_url
通知地址 这又是一个坑,好多人不理解是干吗的,这是接收微信支付异步通知回调地址用的,通知url必须为直接可访问的url,不能携带参数! 也能够这样理解,这个是给微信支付的接口,微信来调用的接口,微信调这接口干吗用呢?就是告诉你用户付款成功啦或者用户付款失败了,而后你就能够在这个接口里经过微信给你返回的信息来作逻辑处理了。
trade_type
固定值 写 “APP” 由于咱写的是APP支付嘛,因此就填APP。
好了就是这些必选参数了,剩下就能够本身选择是否要用的参数了根据本身状况而定。
参数选完了就要发送参数了呗,如何发呢?
咱们来调用wechatAppPay类中的unifiedOrder()函数。
啊哈啥!!!!!!??????
(⊙o⊙)?(⊙o⊙)?(⊙o⊙)?(⊙o⊙)?(⊙o⊙)?
wechatAppPay类???unifiedOrder()函数???
对就这这俩东西,不要惊讶,不要着急看最后有代码,有这个类,有代码的O(∩_∩)O哈哈~,
你只需在你的项目中加载这个类就能够调用这个方法了!不要崇拜我( ╯▽╰)(由于这个类不是我写的我也忘了从哪找的了,我从百度搜的而后整理的作了些改动╮(╯▽╰)╭ -_-|||-_-|||-_-!好吧好吧好吧没作改动,只是加了点注释而已,感谢写这个类的大神谢谢O(∩_∩)O谢谢O(∩_∩)O谢谢)
好了抽完疯了,开是干正事!
咱们先来new下wechatAppPay类
$wxappid = 'wx0000000000000';//应用ID 字符串 $mch_id = '1000000000';//商户号 字符串 $notify_url = 'http://www.xxx.com/xxxx.php/xxxx/xxxx';//接收微信支付异步通知回调地址 字符串 $wxkey = '00000000000000000000000';//这个是在商户中心设置的那个值用来生成签名时保证安全的 字符串 $this->wechatAppPay = new wechatAppPay($wxappid, $mch_id, $notify_url, $wxkey);
调用wechatAppPay类中的unifiedOrder()函数。unifiedOrder()须要的参数是个数组咱们定义为$params
$params = array(); $params['body'] = 'APP-在线支付'; //必填项 商品描述 $params['out_trade_no'] = time()."$member"; //必填项 自定义的订单号 $params['total_fee'] = ($money*100); //必填项 订单金额 单位为分因此要*100 $params['trade_type'] = 'APP'; //必填项 交易类型固定写 APP $params['根据本身状况定的值'] = "根据本身状况定的值" //非必填项 根据本身状况定的值 这个可有好多个能够参看开发文档中的参数 $result = $this->wechatAppPay->unifiedOrder( $params );
注:若是你加了$params['根据本身状况定的值'] wechatAppPay类里要作相应的改动,文章的最后有代码,你一看代码就明白了
如今$result就是咱们调用统一下单接口返回的数据了,这个$resutl经过unifiedOrder()函数的处理已经把xml格式变成数组了。$result 里有return_code,return_msg,appid,mch_id,nonce_str,sign,result_code,prepay_id,trade_type。这里面就用一个prepay_id(预支付交易会话ID),其余都不重要了
步骤2完毕
步骤3 把数据返回给商户APP应用(移动端)调起支付接口
如今咱们要把调用统一下单接口返回的数据$resutl里的几个值返回给移动端那几个值呢?这几个:
appid
应用ID 这个是固定的 能够本身写也能够从$resutl里拿 可让移动端写死 就不用每次返回了
partnerid
商户号 这个也是固定的 能够本身写也能够从$resutl里拿 可让移动端写死 就不用每次返回了
prepayid
预支付交易会话ID 这个很重要必须返回给移动端 是必须从$resutl里拿的
package
扩展字段 能够本身写也能够从$resutl里拿 暂填写固定值"Sign=WXPay" 可让移动端写死 就不用每次返回了
noncestr
随机字符串 这个能够本身写也能够从$resutl里拿
timestamp
时间戳 本身生成 标准北京时间,时区为东八区注意:部分系统取到的值为毫秒级,须要转换成秒(10位数字),这里有个坑,ISO端接收的时候好像得强行转化一下,由于返回的是字符串不是数字,还有什么几位的数字之类的,我也不太懂,反正就是写的时候提醒下iOS工程师就行。安卓不清楚。
sign
签名 又来一个坑,咱们把这个签名叫作二次签名,可是这个签名不是从$resutl里拿的,而是本身写的,如何写呢,又有坑!由于参与签名的参数值是那几个不清楚,参数名写不对!不怕我有代码!贴给你看!须要参与签名的值有六个! $sign_array = array(); $sign_array['appid'] = $wx_result['appid']; //注意 $sign_array['appid'] 里的参数名必须是appid $sign_array['partnerid'] = $wx_result['mch_id']; //注意 $sign_array['partnerid'] 里的参数名必须是partnerid $sign_array['prepayid'] = $wx_result['prepay_id'];//注意 $sign_array['prepayid'] 里的参数名必须是prepayid $sign_array['package'] = 'Sign=WXPay'; //注意 $sign_array['package'] 里的参数名必须是package $sign_array['noncestr'] = $wx_result['nonce_str'];//注意 $sign_array['noncestr'] 里的参数名必须是noncestr $sign_array['timestamp'] = time(); //注意 $sign_array['timestamp'] 里的参数名必须是timestamp $sign_two = $this->wechatAppPay->MakeSign($sign_array);//调用wechatAppPay类里的MakeSign()函数生成sign
如今就能够把从新生成的sign($sign_two)以及其余参数返回给移动端了,一共返回七个值,有三个之可让前端写死(appid,partnerid,package),其他四个必须由服务器返回给移动端。
步骤3完毕
步骤4由移动端完成
步骤5 回调接口 支付结果通用通知
还记得步骤2中咱们设置的$notify_url吗,对如今就要对这个微信返回到这个接口的数据进行一系列的逻辑处理了官方是这样写的:
支付完成后,微信会把相关支付结果和用户信息发送给商户,商户须要接收处理,并返回应答。 对后台通知交互时,若是微信收到商户的应答不是成功或超时,微信认为通知失败,微信会经过必定的策略按期从新发起通知,尽量提升通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒) 注意:一样的通知可能会屡次发送给商户系统。商户系统必须可以正确处理重复的通知。 推荐的作法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,若是没有处理过再进行处理,若是处理过直接返回结果成功。在对业务数据进行状态检查和处理以前,要采用数据锁进行并发控制,以免函数重入形成的数据混乱。 特别提醒:商户系统对于支付结果通知的内容必定要作签名验证,防止数据泄漏致使出现“假通知”,形成资金损失。
首先来接收数据
$data = $this->wechatAppPay->getNotifyData();//获取数据 用wechatAppPay类里的getNotifyData()方法,这里数据也被getNotifyData()由xml转化成了数组。
而后官方说要采用数据锁进行并发控制,这个我不懂因此没写(若是你懂你会的话请给我留言私信告诉我,在这谢谢了),对数据进行状态检查这个写了,如何写的呢?很简单微信返回的值有好多其中就能够判断result_code(业务结果)和return_code(返回状态码)是否为SUCCESS就能够了代码就不写了。
而后验签,这个很重要由于这是保证数据没有被第三方人为篡改的标准!
如何验签呢?
把返回的数据$data里除去sign剩下的值都参与从新签名咱们把此次签名叫作验签签名,验签签名生成后再与$data里的sign对比,若是相同验签经过,不然不经过。此次签名的参数名与二次签名时的参数名不一样,$data数组里叫什么参数名就验签时叫什么参数名。听乱了吧?(~ ̄▽ ̄)~(~ ̄▽ ̄)~不要紧请看代码
//假如$data里有以下参数 $w_sign = array(); //参加验签签名的参数数组 $w_sign['appid'] = $data['appid']; $w_sign['bank_type'] = $data['bank_type']; $w_sign['cash_fee'] = $data['cash_fee']; $w_sign['fee_type'] = $data['fee_type']; $w_sign['is_subscribe'] = $data['is_subscribe']; $w_sign['mch_id'] = $data['mch_id']; $w_sign['nonce_str'] = $data['nonce_str']; $w_sign['openid'] = $data['openid']; $w_sign['out_trade_no'] = $data['out_trade_no']; $w_sign['result_code'] = $data['result_code']; $w_sign['return_code'] = $data['return_code']; $w_sign['time_end'] = $data['time_end']; $w_sign['total_fee'] = $data['total_fee']; $w_sign['trade_type'] = $data['trade_type']; $w_sign['transaction_id'] = $data['transaction_id']; $verify_sign = $this->wechatAppPay->MakeSign($w_sign);//生成验签签名
好了如今假设你的验签已经经过了接下里就是你本身的逻辑处理了
/////////////////////////////////////////////////////// 商户APP应用(后台)处理逻辑代码 //////////////////////////////////////////////////////
本身的逻辑处理已经处理完以后,还得告诉微信一下,别再一直发结果通用通知啦,我已经收到通知并处理完啦!
$this->wechatAppPay->replyNotify();//商户处理后同步返回给微信参数
步骤5结束步骤6根据本身状况而定
至此微信支付处理完成~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦
写的有不对的方还请你们多多指导指教!!!给我留言!!b( ̄▽ ̄)db( ̄▽ ̄)db( ̄▽ ̄)d
还有感谢在我写微信支付地时候 那些被我问烦了的大神们! !谢谢啦~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦O(∩_∩)O哈哈~O(∩_∩)O哈哈~O(∩_∩)O哈哈~<( ̄︶ ̄)><( ̄︶ ̄)><( ̄︶ ̄)>
wechatAppPay类 重头戏
class wechatAppPay { //接口API URL前缀 const API_URL_PREFIX = 'https://api.mch.weixin.qq.com'; //下单地址URL const UNIFIEDORDER_URL = "/pay/unifiedorder"; //查询订单URL const ORDERQUERY_URL = "/pay/orderquery"; //关闭订单URL const CLOSEORDER_URL = "/pay/closeorder"; //公众帐号ID private $wxappid; //商户号 private $mch_id; //随机字符串 private $nonce_str; //签名 private $sign; //商品描述 private $body; //商户订单号 private $out_trade_no; //支付总金额 private $total_fee; //终端IP private $spbill_create_ip; //支付结果回调通知地址 private $notify_url; //交易类型 private $trade_type; //支付密钥 private $key; //证书路径 private $SSLCERT_PATH; private $SSLKEY_PATH; //全部参数 private $params = array(); public function __construct($wxappid, $mch_id, $notify_url, $key) { $this->appid = $wxappid; $this->mch_id = $mch_id; $this->notify_url = $notify_url; $this->key = $key; } /** * 下单方法 * @param $params 下单参数 */ public function unifiedOrder( $params ){ $this->body = $params['body']; $this->out_trade_no = $params['out_trade_no']; $this->total_fee = $params['total_fee']; $this->trade_type = $params['trade_type']; $this->nonce_str = $this->genRandomString(); $this->spbill_create_ip = $_SERVER['REMOTE_ADDR']; $this->params['appid'] = $this->appid; $this->params['mch_id'] = $this->mch_id; $this->params['nonce_str'] = $this->nonce_str; $this->params['body'] = $this->body; $this->params['out_trade_no'] = $this->out_trade_no; $this->params['total_fee'] = $this->total_fee; $this->params['spbill_create_ip'] = $this->spbill_create_ip; $this->params['notify_url'] = $this->notify_url; $this->params['trade_type'] = $this->trade_type; //获取签名数据 $this->sign = $this->MakeSign( $this->params ); $this->params['sign'] = $this->sign; $xml = $this->data_to_xml($this->params); $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::UNIFIEDORDER_URL); if( !$response ){ return false; } $result = $this->xml_to_data( $response ); if( !empty($result['result_code']) && !empty($result['err_code']) ){ $result['err_msg'] = $this->error_code( $result['err_code'] ); } return $result; } /** * 查询订单信息 * @param $out_trade_no 订单号 * @return array */ public function orderQuery( $out_trade_no ){ $this->params['appid'] = $this->appid; $this->params['mch_id'] = $this->mch_id; $this->params['nonce_str'] = $this->genRandomString(); $this->params['out_trade_no'] = $out_trade_no; //获取签名数据 $this->sign = $this->MakeSign( $this->params ); $this->params['sign'] = $this->sign; $xml = $this->data_to_xml($this->params); $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::ORDERQUERY_URL); if( !$response ){ return false; } $result = $this->xml_to_data( $response ); if( !empty($result['result_code']) && !empty($result['err_code']) ){ $result['err_msg'] = $this->error_code( $result['err_code'] ); } return $result; } /** * 关闭订单 * @param $out_trade_no 订单号 * @return array */ public function closeOrder( $out_trade_no ){ $this->params['appid'] = $this->appid; $this->params['mch_id'] = $this->mch_id; $this->params['nonce_str'] = $this->genRandomString(); $this->params['out_trade_no'] = $out_trade_no; //获取签名数据 $this->sign = $this->MakeSign( $this->params ); $this->params['sign'] = $this->sign; $xml = $this->data_to_xml($this->params); $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::CLOSEORDER_URL); if( !$response ){ return false; } $result = $this->xml_to_data( $response ); return $result; } /** * * 获取支付结果通知数据 * return array */ public function getNotifyData(){ //获取通知的数据 $xml = $GLOBALS['HTTP_RAW_POST_DATA']; $data = array(); if( empty($xml) ){ return false; } $data = $this->xml_to_data( $xml ); if( !empty($data['return_code']) ){ if( $data['return_code'] == 'FAIL' ){ return false; } } return $data; } /** * 接收通知成功后应答输出XML数据 * @param string $xml */ public function replyNotify(){ $data['return_code'] = 'SUCCESS'; $data['return_msg'] = 'OK'; $xml = $this->data_to_xml( $data ); echo $xml; die(); } /** * 生成APP端支付参数 * @param $prepayid 预支付id */ public function getAppPayParams( $prepayid ){ $data['appid'] = $this->appid; $data['partnerid'] = $this->mch_id; $data['prepayid'] = $prepayid; $data['package'] = 'Sign=WXPay'; $data['noncestr'] = $this->genRandomString(); $data['timestamp'] = time(); $data['sign'] = $this->MakeSign( $data ); return $data; } /** * 生成签名 * @return 签名 */ public function MakeSign( $params ){ //签名步骤一:按字典序排序数组参数 ksort($params); $string = $this->ToUrlParams($params); //签名步骤二:在string后加入KEY $string = $string . "&key=".$this->key; //签名步骤三:MD5加密 $string = md5($string); //签名步骤四:全部字符转为大写 $result = strtoupper($string); return $result; } /** * 将参数拼接为url: key=value&key=value * @param $params * @return string */ public function ToUrlParams( $params ){ $string = ''; if( !empty($params) ){ $array = array(); foreach( $params as $key => $value ){ $array[] = $key.'='.$value; } $string = implode("&",$array); } return $string; } /** * 输出xml字符 * @param $params 参数名称 * return string 返回组装的xml **/ public function data_to_xml( $params ){ if(!is_array($params)|| count($params) <= 0) { return false; } $xml = "<xml>"; foreach ($params as $key=>$val) { if (is_numeric($val)){ $xml.="<".$key.">".$val."</".$key.">"; }else{ $xml.="<".$key."><![CDATA[".$val."]]></".$key.">"; } } $xml.="</xml>"; return $xml; } /** * 将xml转为array * @param string $xml * return array */ public function xml_to_data($xml){ if(!$xml){ return false; } //将XML转为array //禁止引用外部xml实体 libxml_disable_entity_loader(true); $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); return $data; } /** * 获取毫秒级别的时间戳 */ private static function getMillisecond(){ //获取毫秒的时间戳 $time = explode ( " ", microtime () ); $time = $time[1] . ($time[0] * 1000); $time2 = explode( ".", $time ); $time = $time2[0]; return $time; } /** * 产生一个指定长度的随机字符串,并返回给用户 * @param type $len 产生字符串的长度 * @return string 随机字符串 */ private function genRandomString($len = 32) { $chars = array( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ); $charsLen = count($chars) - 1; // 将数组打乱 shuffle($chars); $output = ""; for ($i = 0; $i < $len; $i++) { $output .= $chars[mt_rand(0, $charsLen)]; } return $output; } /** * 以post方式提交xml到对应的接口url * * @param string $xml 须要post的xml数据 * @param string $url url * @param bool $useCert 是否须要证书,默认不须要 * @param int $second url执行超时时间,默认30s * @throws WxPayException */ private function postXmlCurl($xml, $url, $useCert = false, $second = 30){ $ch = curl_init(); //设置超时 curl_setopt($ch, CURLOPT_TIMEOUT, $second); curl_setopt($ch,CURLOPT_URL, $url); curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE); curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,2); //设置header curl_setopt($ch, CURLOPT_HEADER, FALSE); //要求结果为字符串且输出到屏幕上 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); if($useCert == true){ //设置证书 //使用证书:cert 与 key 分别属于两个.pem文件 curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM'); //curl_setopt($ch,CURLOPT_SSLCERT, WxPayConfig::SSLCERT_PATH); curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM'); //curl_setopt($ch,CURLOPT_SSLKEY, WxPayConfig::SSLKEY_PATH); } //post提交方式 curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); //运行curl $data = curl_exec($ch); //返回结果 if($data){ curl_close($ch); return $data; } else { $error = curl_errno($ch); curl_close($ch); return false; } } /** * 错误代码 * @param $code 服务器输出的错误代码 * return string */ public function error_code( $code ){ $errList = array( 'NOAUTH' => '商户未开通此接口权限', 'NOTENOUGH' => '用户账号余额不足', 'ORDERNOTEXIST' => '订单号不存在', 'ORDERPAID' => '商户订单已支付,无需重复操做', 'ORDERCLOSED' => '当前订单已关闭,没法支付', 'SYSTEMERROR' => '系统错误!系统超时', 'APPID_NOT_EXIST' => '参数中缺乏APPID', 'MCHID_NOT_EXIST' => '参数中缺乏MCHID', 'APPID_MCHID_NOT_MATCH' => 'appid和mch_id不匹配', 'LACK_PARAMS' => '缺乏必要的请求参数', 'OUT_TRADE_NO_USED' => '同一笔交易不能屡次提交', 'SIGNERROR' => '参数签名结果不正确', 'XML_FORMAT_ERROR' => 'XML格式错误', 'REQUIRE_POST_METHOD' => '未使用post传递参数 ', 'POST_DATA_EMPTY' => 'post数据不能为空', 'NOT_UTF8' => '未使用指定编码格式', ); if( array_key_exists( $code , $errList ) ){ return $errList[$code]; } } }