? PHP实现微信支付及退款流程的实例详解

  PHP实现微信支付及退款流程实例

  微信小程序支付的主要逻辑集中在后端,前端只需携带支付所需的数据请求后端接口然后根据返回结果做相应成功失败处理即可。本篇文章后端使用的是php,侧重于整个支付的流程和一些细节方面的东西。所以使用其他后端语言的朋友有需要也是可以看一下的。很多时候开发的需求和相应问题的解决真的要跳出语言语法层面,去从系统和流程的角度考虑。

  一、微信支付

  支付主要分为几个步骤:

  前端携带支付需要的数据(商品id,购买数量等)发起支付请求

  后端在接收到支付请求后,处理支付数据,然后携带处理后的数据请求 微信服务器 的 支付统一下单接口

  后端接收到上一步请求微信服务器的返回数据,再次处理,然后返回前端让前端可以开始支付。

  前端进行支付动作

  前端支付完成后,微信服务器会向后端发送支付通知(也就是微信要告诉你客户已经付过钱了),后端根据这个通知确定支付完成,然后就去做支付完成后的相应动作,比如修改订单状态,添加交易日志啊等等。

  从这几个步骤可以看出,后端主要的作用就是将支付需要的数据传给微信服务器,再根据微信服务器的响应确定支付是否完成。

  这个流程还是蛮容易理解的。形象的说,前端就是个顾客,后端就是店家,微信服务器的统一下单接口就像收银员。顾客跟店家说,我是谁谁谁,现在我要付多少多少钱给你买什么什么。店家就跟收银员说,那个谁谁谁要付多少钱,你准备收钱吧。收银员收到钱后,就去告诉店家,我已经收到钱了,你给他东西吧。

  下面就详细的说明一下各个步骤的具体实现。

  1. 前端请求支付

  前端请求支付,就是简单的携带支付需要的数据,例如用户标识,支付金额,支付订单 ID 等等跟 **你的业务逻辑有关** 或者跟 **下一步请求微信服务器支付统一下单接口需要的数据有关** 的相关数据,使用微信小程序的 wx.request( ) 去请求后端的支付接口。

  2. 后端请求微信服务器

  后端接收到前端发送的支付请求后,可以进行一下相关验证,例如判断一下用户有没有问题,支付金额对不对等等。

  在验证没什么问题,可以向微信服务器申请支付之后,后端需要使用 微信规定的数据格式 去请求微信的支付统一下单接口。

  微信规定的请求数据:

  这需要较多代码实现。因为需要的数据个数较多,而且还需要加密并以 XML 格式发送。

  首先,有以下数据是使用小程序支付必须提供给微信服务器的参数。

  小程序 appid。写小程序的大概没有不知道这个的。。。

  用户标识 openid。也就是用户的小程序标识,在我上篇博客中说明了如何获取。

  商户号 mch_id 。申请开通微信支付商户认证成功后微信发给你的邮件里有

  商户订单号 out_trade_no 。商户为这次支付生成的订单号

  总金额 total_fee 。订单总金额,很重要的一点是单位是分,要特别注意。

  微信服务器回调通知接口地址 notify_url。微信确认钱已经到账后,会往这个地址多次发送消息,告诉你顾客已经付完钱了,你需要返回消息给微信表示你已经收到了通知。。这个地址不能有端口号,同时要能直接接受POST方法请求。

  交易类型 trade_type 。微信小程序支付此值统一为 JSAPI

  商品信息 Body。类似"腾讯-游戏"这种格式

  终端IP地址 spbill_create_ip 。终端地址IP,也就是请求支付的 IP 地址。

  随机字符串 nonce_str 。需要后端随机生成的字符串用于保证数据安全。微信要求不长于32位。

  签名 sign 。使用上面的所有参数进行相应处理加密生成签名。(具体处理方式可见下文代码,可直接复用。)

  在处理好以上所有数据后,将这些数据以 XML 格式整理并以 POST 方法发送到 微信支付统一下单接口

  3.后端接受微信服务器返回数据

  微信服务器在接收到支付数据之后,如果数据没有问题,其会返回用于支付的相应数据,其中非常重要的是 名称为 prepay_id 的数据字段,需要将此数据返回前端,前端才能继续支付。

  因此,在后端接收到微信服务器的返回数据后,需要进行相应的处理,最终返回到前端如下数据:

  appid 不需多说

  timeStamp 当前时间戳

  nonceStr 随机字符串

  package 就是上面提到的 prepay_id,不过切记格式如 “prepay_id= prepay_id_item“。否则会导致错误。

  signType 加密方式,一般应该是 MD5

  paySign 对以上数据进行相应处理并加密。

  到这里,后端的支付接口已经完成了接收前端支付请求,并返回了前端支付所需数据的功能。

  4. 前端发起支付

  前端在接收到返回数据后,使用 wx.requestPayment() 来请求发起支付。此 API 需要的对象参数各项值就是我们上一步返回的各个数据。

  5.后端接受微信服务器回调

  前端完成支付后,微信服务器确认支付已经完成。就会向第一步中设置的回调地址发送通知。后端的接收回调接口在接收到通知后,就可以判断支付是否完成,从而决定后续动作。

  需要注意的是,在接收到微信服务器的回调通知后,根据通知的result_code字段判断支付是否成功。在接受到成功的通知后,后端需要返回success数据向微信服务器告知已得到回调通知。否则微信服务器会不停的向后端发送消息。另外微信的通知是以XML格式发送的,在接受处理时需要注意。

  微信的大概支付流程就是这样。以下是PHP语法的微信支付类,可以比照上面的步骤介绍,加深理解。在需要支付时,直接传入参数实例化此类再调用类的 pay 方法即可。

  //微信支付类

  1 class WeiXinPay{
  2 
  3   //=======【基本信息设置】=====================================
  4 
  5   //微信公众号身份的唯一标识
  6 
  7   protected $APPID = appid;//填写您的appid。微信公众平台里的
  8 
  9   protected $APPSECRET = secret;
 10 
 11   //受理商ID,身份标识
 12 
 13   protected $MCHID = ‘11111111‘;//商户id
 14 
 15   //商户支付密钥Key
 16 
 17   protected $KEY = ‘192006250b4c09247ec02edce69f6a2d‘;
 18 
 19   //回调通知接口
 20 
 21   protected $APPURL =   ‘https://smart.afei.com/receivesuc‘;
 22 
 23   //交易类型
 24 
 25   protected $TRADETYPE = ‘JSAPI‘;
 26 
 27   //商品类型信息
 28 
 29   protected $BODY = ‘wx/book‘;
 30 
 31   //微信支付类的构造函数
 32 
 33   function __construct($openid,$outTradeNo,$totalFee){
 34 
 35     $this->openid = $openid; //用户唯一标识
 36 
 37     $this->outTradeNo = $outTradeNo; //商品编号
 38 
 39     $this->totalFee = $totalFee; //总价
 40 
 41   }
 42 
 43   //微信支付类向外暴露的支付接口
 44 
 45   public function pay(){
 46 
 47     $result = $this->weixinapp();
 48 
 49     return $result;
 50 
 51   }
 52 
 53    //对微信统一下单接口返回的支付相关数据进行处理
 54 
 55    private function weixinapp(){
 56 
 57      $unifiedorder=$this->unifiedorder();
 58 
 59      $parameters=array(
 60 
 61      ‘appId‘=>$this->APPID,//小程序ID
 62 
 63      ‘timeStamp‘=>‘‘.time().‘‘,//时间戳
 64 
 65      ‘nonceStr‘=>$this->createNoncestr(),//随机串
 66 
 67      ‘package‘=>‘prepay_id=‘.$unifiedorder[‘prepay_id‘],//数据包
 68 
 69      ‘signType‘=>‘MD5‘//签名方式
 70 
 71        );
 72 
 73      $parameters[‘paySign‘]=$this->getSign($parameters);
 74 
 75      return $parameters;
 76 
 77    }
 78 
 79   /*
 80 
 81    *请求微信统一下单接口
 82 
 83    */
 84 
 85   private function unifiedorder(){
 86 
 87     $parameters = array(
 88 
 89       ‘appid‘ => $this->APPID,//小程序id
 90 
 91       ‘mch_id‘=> $this->MCHID,//商户id
 92 
 93       ‘spbill_create_ip‘=>$_SERVER[‘REMOTE_ADDR‘],//终端ip
 94 
 95       ‘notify_url‘=>$this->APPURL, //通知地址
 96 
 97       ‘nonce_str‘=> $this->createNoncestr(),//随机字符串
 98 
 99       ‘out_trade_no‘=>$this->outTradeNo,//商户订单编号
100 
101       ‘total_fee‘=>floatval($this->totalFee), //总金额
102 
103       ‘open_id‘=>$this->openid,//用户openid
104 
105       ‘trade_type‘=>$this->TRADETYPE,//交易类型
106 
107       ‘body‘ =>$this->BODY, //商品信息
108 
109     );
110 
111     $parameters[‘sign‘] = $this->getSign($parameters);
112 
113     $xmlData = $this->arrayToXml($parameters);
114 
115     $xml_result = $this->postXmlCurl($xmlData,‘https://api.mch.weixin.qq.com/pay/unifiedorder‘,60);
116 
117     $result = $this->xmlToArray($xml_result);
118 
119     return $result;
120 
121   }
122 
123   //数组转字符串方法
124 
125   protected function arrayToXml($arr){
126 
127     $xml = "<xml>";
128 
129     foreach ($arr as $key=>$val)
130 
131     {
132 
133       if (is_numeric($val)){
134 
135         $xml.="<".$key.">".$val."</".$key.">";
136 
137       }else{
138 
139          $xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
140 
141       }
142 
143     }
144 
145     $xml.="</xml>";
146 
147     return $xml;
148 
149   }
150 
151   protected function xmlToArray($xml){
152 
153     $array_data = json_decode(json_encode(simplexml_load_string($xml, ‘SimpleXMLElement‘, LIBXML_NOCDATA)), true);
154 
155     return $array_data;
156 
157   }
158 
159   //发送xml请求方法
160 
161   private static function postXmlCurl($xml, $url, $second = 30)
162 
163   {
164 
165     $ch = curl_init();
166 
167     //设置超时
168 
169     curl_setopt($ch, CURLOPT_TIMEOUT, $second);
170 
171     curl_setopt($ch, CURLOPT_URL, $url);
172 
173     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
174 
175     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); //严格校验
176 
177     //设置header
178 
179     curl_setopt($ch, CURLOPT_HEADER, FALSE);
180 
181     //要求结果为字符串且输出到屏幕上
182 
183     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
184 
185     //post提交方式
186 
187     curl_setopt($ch, CURLOPT_POST, TRUE);
188 
189     curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
190 
191     curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
192 
193     curl_setopt($ch, CURLOPT_TIMEOUT, 40);
194 
195     set_time_limit(0);
196 
197     //运行curl
198 
199     $data = curl_exec($ch);
200 
201     //返回结果
202 
203     if ($data) {
204 
205       curl_close($ch);
206 
207       return $data;
208 
209     } else {
210 
211       $error = curl_errno($ch);
212 
213       curl_close($ch);
214 
215       throw new WxPayException("curl出错,错误码:$error");
216 
217     }
218 
219   }
220 
221   /*
222 
223    * 对要发送到微信统一下单接口的数据进行签名
224 
225    */
226 
227   protected function getSign($Obj){
228 
229      foreach ($Obj as $k => $v){
230 
231      $Parameters[$k] = $v;
232 
233      }
234 
235      //签名步骤一:按字典序排序参数
236 
237      ksort($Parameters);
238 
239      $String = $this->formatBizQueryParaMap($Parameters, false);
240 
241      //签名步骤二:在string后加入KEY
242 
243      $String = $String."&key=".$this->KEY;
244 
245      //签名步骤三:MD5加密
246 
247      $String = md5($String);
248 
249      //签名步骤四:所有字符转为大写
250 
251      $result_ = strtoupper($String);
252 
253      return $result_;
254 
255    }
256 
257   /*
258 
259    *排序并格式化参数方法,签名时需要使用
260 
261    */
262 
263   protected function formatBizQueryParaMap($paraMap, $urlencode)
264 
265   {
266 
267     $buff = "";
268 
269     ksort($paraMap);
270 
271     foreach ($paraMap as $k => $v)
272 
273     {
274 
275       if($urlencode)
276 
277       {
278 
279         $v = urlencode($v);
280 
281       }
282 
283       //$buff .= strtolower($k) . "=" . $v . "&";
284 
285       $buff .= $k . "=" . $v . "&";
286 
287     }
288 
289     $reqPar;
290 
291     if (strlen($buff) > 0)
292 
293     {
294 
295       $reqPar = substr($buff, 0, strlen($buff)-1);
296 
297     }
298 
299     return $reqPar;
300 
301   }
302 
303   /*
304 
305    * 生成随机字符串方法
306 
307    */
308 
309   protected function createNoncestr($length = 32 ){
310 
311      $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
312 
313      $str ="";
314 
315      for ( $i = 0; $i < $length; $i++ ) {
316 
317      $str.= substr($chars, mt_rand(0, strlen($chars)-1), 1);
318 
319      }
320 
321      return $str;
322 
323      }
324 
325 }

  以上就是微信支付的相关流程。在理清思路后,流程还是比较清晰和简单的。重点在于需要注意一些细节问题,例如数据格式,加密方法等。

  下面说一下微信小程序退款的具体实现

  二、微信退款

  小程序退款的流程和付款相似,但有一些细节上的不同。

  首先退款的步骤通常如下:

  用户前端点击退款按钮后,后端接收到用户的退款请求通过商城后台呈现给商户,商户确定允许退款后,后端再发起向微信退款接口的请求来请求退款。

  后端向微信退款接口发送请求后,得到响应信息,确定退款是否完成,根据退款是否完成再去进行改变订单状态等业务逻辑。

  退款的步骤相对微信支付来说比较简单。

  值得注意的有以下两点:

  1.向微信退款接口请求退款后,根据得到的响应是可以直接确定退款是否完成的。不再需要设置专门的回调接口等待微信通知。当然如果需要也是可以在微信商户平台设置回调接口接受从而接受微信回调的,但并不是必须的。

  2.退款请求需要在请求服务器安装微信提供的安全证书,也就是说,发起退款请求相比较支付请求在请求时请求方法不能复用,因为微信退款需要携带证书的请求,此证书可在申请微信商户号成功后从微信商户平台自行下载,Linux下的PHP开发环境的证书只需要放在网站根目录的cert文件夹中即可。其他开发环境可能需要导入操作。

  下面讲解一下退款的具体步骤

  一. 用户发起退款请求

  用户在前端发起退款请求,后端接收到退款请求,将相应订单标记为申请退款,展示在后台.商户查看后,如果同意退款再进行相应操作.此后才进入真正的退款流程.

  二. 商户发起退款请求

  商户同意退款后,后端即向微信提供的退款 API 发起请求.

  同请求微信支付API一样.退款请求也需要将需要的参数进行签名后以XML发送到微信的退款API

  退款请求需要的参数如下(多个参数在支付API请求时也有使用):

  参数:

  appid

  mch_id 商户号。申请开通微信支付商户认证成功后微信发给你的邮件里有

  out_trade_no 商户订单号。退款订单在支付时生成的订单号

  out_refund_no 退款订单号。由后端生成的退款单号,需要保证唯一,因为多个同样的退款单号只会退款一次。

  total_fee 总金额。订单总金额,单位为分。

  refund_fee 退款金额。 需要退款的金额,单位同样为分

  op_user_id 操作员。与商户号相同即可

  nonce_str 随机字符串。同支付请求

  签名 sign 。使用上面的所有参数进行相应处理加密生成签名。(具体处理方式与支付相同,可直接复用。)

  注:我就是因为在实例化的时候没有传入key(秘钥)参数,一直报错签名错误。

  三. 退款完成

  在发起退款请求后,就可以直接根据请求的响应XML中的 result_code字段来判断退款是否成功,从而对订单状态进行处理和后续操作。不需要像支付那样等待另一个接口的通知来确定请求状态。当然如上文所说,如果需要微信服务器发送通知到后端的话,可以到微信商户平台进行设置。

  退款因为流程与支付大同小异,因此退款的PHP类我选择了直接继承支付类,

  代码如下,注意区分退款请求方法postXmlSSLCurl和支付请求方法postXmlCurl的区别,这也就是上文提到的退款需要的双向证书的使用。

  1 class WinXinRefund extends WeiXinPay{
  2 
  3   protected \$SSLCERT_PATH = ‘cert/apiclient_cert.pem‘;//证书路径
  4 
  5   protected \$SSLKEY_PATH = ‘cert/apiclient_key.pem‘;//证书路径
  6 
  7   protected \$opUserId = ‘1234567899‘;//商户号
  8 
  9 function __construct($openid,$outTradeNo,$totalFee,$outRefundNo,$refundFee){
 10 
 11   //初始化退款类需要的变量
 12 
 13   $this->openid = $openid;
 14 
 15   $this->outTradeNo = $outTradeNo;
 16 
 17   $this->totalFee = $totalFee;
 18 
 19   $this->outRefundNo = $outRefundNo;
 20 
 21   $this->refundFee = $refundFee;
 22 
 23 } 
 24 
 25 public function refund(){
 26 
 27   //对外暴露的退款接口
 28 
 29   $result = $this->wxrefundapi();
 30 
 31   return $result;
 32 
 33 }
 34 
 35 private function wxrefundapi(){
 36 
 37   //通过微信api进行退款流程
 38 
 39   $parma = array(
 40 
 41     ‘appid‘=> $this->APPID,
 42 
 43     ‘mch_id‘=> $this->MCHID,
 44 
 45     ‘nonce_str‘=> $this->createNoncestr(),
 46 
 47     ‘out_refund_no‘=> $this->outRefundNo,
 48 
 49     ‘out_trade_no‘=> $this->outTradeNo,
 50 
 51     ‘total_fee‘=> $this->totalFee,
 52 
 53     ‘refund_fee‘=> $this->refundFee,
 54 
 55     ‘op_user_id‘ => $this->opUserId,
 56 
 57   );
 58 
 59   $parma[‘sign‘] = $this->getSign($parma);
 60 
 61   $xmldata = $this->arrayToXml($parma);
 62 
 63   $xmlresult = $this->postXmlSSLCurl($xmldata,‘https://api.mch.weixin.qq.com/secapi/pay/refund‘);
 64 
 65   $result = $this->xmlToArray($xmlresult);
 66 
 67   return $result;
 68 
 69 }
 70 
 71 //需要使用证书的请求
 72 
 73 function postXmlSSLCurl($xml,$url,$second=30)
 74 
 75 {
 76 
 77   $ch = curl_init();
 78 
 79   //超时时间
 80 
 81   curl_setopt($ch,CURLOPT_TIMEOUT,$second);
 82 
 83   //这里设置代理,如果有的话
 84 
 85   //curl_setopt($ch,CURLOPT_PROXY, ‘8.8.8.8‘);
 86 
 87   //curl_setopt($ch,CURLOPT_PROXYPORT, 8080);
 88 
 89   curl_setopt($ch,CURLOPT_URL, $url);
 90 
 91   curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
 92 
 93   curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);
 94 
 95   //设置header
 96 
 97   curl_setopt($ch,CURLOPT_HEADER,FALSE);
 98 
 99   //要求结果为字符串且输出到屏幕上
100 
101   curl_setopt($ch,CURLOPT_RETURNTRANSFER,TRUE);
102 
103   //设置证书
104 
105   //使用证书:cert 与 key 分别属于两个.pem文件
106 
107   //默认格式为PEM,可以注释
108 
109   curl_setopt($ch,CURLOPT_SSLCERTTYPE,‘PEM‘);
110 
111   curl_setopt($ch,CURLOPT_SSLCERT, $this->SSLCERT_PATH);
112 
113   //默认格式为PEM,可以注释
114 
115   curl_setopt($ch,CURLOPT_SSLKEYTYPE,‘PEM‘);
116 
117   curl_setopt($ch,CURLOPT_SSLKEY, $this->SSLKEY_PATH);
118 
119   //post提交方式
120 
121   curl_setopt($ch,CURLOPT_POST, true);
122 
123   curl_setopt($ch,CURLOPT_POSTFIELDS,$xml);
124 
125   $data = curl_exec($ch);
126 
127   //返回结果
128 
129   if($data){
130 
131     curl_close($ch);
132 
133     return $data;
134 
135   }
136 
137   else {
138 
139     $error = curl_errno($ch);
140 
141     echo "curl出错,错误码:$error"."<br>";
142 
143     curl_close($ch);
144 
145     return false;
146 
147   }
148 
149 }}

  三. 总结

  以上就是关于微信支付和退款的流程及相关知识的介绍。文中的 PHP类 均封装直接可用。

  因为微信支付和退款涉及的东西较为繁杂,很多人直接看官方文档可能会一头雾水,所以看过此文了解流程和要点后,再去看微信官方文档。一方面可以更清晰的了解小程序的支付和退款流程。另一方面,本文因为篇幅有限及作者能力有限,肯定有无暇顾及或有所纰漏之处。为求稳妥,还是需要多看看官方开发文档。毕竟事涉支付,出个BUG可不是小事。

 

推荐阅读:[百度seo关键词优化]企业网站快速排名!

? PHP实现微信支付及退款流程的实例详解

上一篇:微信小程序支付服务商版-发起支付


下一篇:点餐小程序怎么申请和认证的方法