⼩程序⽀付及退款流程详解
⼩程序的⽀付和退款流程
近期在做⼩程序时,涉及到了⼩程序的⽀付和退款流程,所以也⼤概的将这⽅⾯的东西看了⼀个遍,就在这篇博客⾥总结⼀下。
⾸先说明⼀下,⼩程序⽀付的主要逻辑集中在后端,前端只需携带⽀付所需的数据请求后端接⼝然后根据返回结果做相应成功失败处理即可。我在后端使⽤的是php,当然在这篇博客⾥我不打算贴
⼀堆代码来说明⽀付的具体实现,⽽主要会侧重于整个⽀付的流程和⼀些细节⽅⾯的东西。所以使⽤其他后端语⾔的朋友有需要也是可以看⼀下的。很多时候开发的需求和相应问题的解决真的要跳出语
⾔语法层⾯,去从系统和流程的⾓度考虑。好的,也不说什么废话了。进⼊正题。
⼀. ⽀付
⽀付主要分为⼏个步骤:
1. 前端携带⽀付需要的数据(商品id,购买数量等)发起⽀付请求
2. 后端在接收到⽀付请求后,处理⽀付数据,然后携带处理后的数据请求服务器的⽀付统⼀下单接⼝
3. 后端接收到上⼀步请求服务器的返回数据,再次处理,然后返回前端让前端可以开始⽀付。
4. 前端进⾏⽀付动作
5. 前端⽀付完成后,服务器会向后端发送⽀付通知(也就是要告诉你客户已经付过钱了),后端根据这个通知确定⽀付完成,然后就去做⽀付完成后的相
应动作,⽐如修改订单状态,添加交易⽇志啊等等。
从这⼏个步骤可以看出,后端主要的作⽤就是将⽀付需要的数据传给服务器,再根据服务器的响应确定⽀付是否完成。
这个流程还是蛮容易理解的。形象的说,前端就是个顾客,后端就是店家,服务器的统⼀下单接⼝就像收银员。顾客跟店家说,我是谁谁谁,现在我要付多少多少钱给你买什么什么。店家就跟收银员说,那个谁谁谁要付多少钱,你准备收钱吧下⾯就详细的说明⼀下各个步骤的具体实现。
1. 前端请求⽀付
前端请求⽀付,就是简单的携带⽀付需要的数据,例如⽤户标识,⽀付⾦额,⽀付订单 ID 等等跟 **你的业务逻辑有关** 或者跟 **下⼀步请求服务器⽀付统⼀下单接⼝需要的数据有关** 的相关数据,使⽤⼩程序的 wx.request( ) 去请求后端2. 后端请求服务器
后端接收到前端发送的⽀付请求后,可以进⾏⼀下相关验证,例如判断⼀下⽤户有没有问题,⽀付⾦额对不对等等。
在验证没什么问题,可以向服务器申请⽀付之后,后端需要使⽤规定的数据格式去请求的⽀付统⼀下单接⼝。
规定的请求数据:
这需要较多代码实现。因为需要的数据个数较多,⽽且还需要加密并以 XML 格式发送。
⾸先,有以下数据是使⽤⼩程序⽀付必须提供给服务器的参数。
1. ⼩程序 appid。写⼩程序的⼤概没有不知道这个的。。。
2. ⽤户标识 openid。也就是⽤户的⼩程序标识,在我中说明了如何获取。
3. 商户号 mch_id 。申请开通⽀付商户认证成功后发给你的邮件⾥有
4. 商户订单号 out_trade_no 。商户为这次⽀付⽣成的订单号
5. 总⾦额 total_fee 。订单总⾦额,很重要的⼀点是单位是分,要特别注意。
6. 服务器回调通知接⼝地址 notify_url。确认钱已经到账后,会往这个地址多次发送消息,告诉你顾客已经付完钱了,你需要返回消息给表⽰你已经
收到了通知。。这个地址不能有端⼝号,同时要能直接接受POST⽅法请求。
7. 交易类型 trade_type 。⼩程序⽀付此值统⼀为 JSAPI
8. 商品信息 Body。类似"腾讯-游戏"这种格式
9. 终端IP地址 spbill_create_ip 。终端地址IP,也就是请求⽀付的 IP 地址。
10. 随机字符串 nonce_str 。需要后端随机⽣成的字符串⽤于保证数据安全。要求不长于32位。
11. 签名 sign 。使⽤上⾯的所有参数进⾏相应处理加密⽣成签名。(具体处理⽅式可见下⽂代码,可直接复⽤。)
3.后端接受服务器返回数据
服务器在接收到⽀付数据之后,如果数据没有问题,其会返回⽤于⽀付的相应数据,其中⾮常重要的是名称为 prepay_id 的数据字段,需要将此数据返回前端,前端才能继
续⽀付。
因此,在后端接收到服务器的返回数据后,需要进⾏相应的处理,最终返回到前端如下数据:
1. appid 不需多说
2. timeStamp 当前时间戳
3. nonceStr 随机字符串
4. package 就是上⾯提到的 prepay_id,不过切记格式如 “prepay_id= prepay_id_item“。否则会导致错误。
5. signType 加密⽅式,⼀般应该是 MD5
6. paySign 对以上数据进⾏相应处理并加密。
到这⾥,后端的⽀付接⼝已经完成了接收前端⽀付请求,并返回了前端⽀付所需数据的功能。
4. 前端发起⽀付
前端在接收到返回数据后,使⽤ wx.requestPayment() 来请求发起⽀付。此 API 需要的对象参数各项值就是我们上⼀步返回的各个数据。
5.后端接受服务器回调
前端完成⽀付后,服务器确认⽀付已经完成。就会向第⼀步中设置的回调地址发送通知。后端的接收回调接⼝在接收到通知后,就可以判断⽀付是否完成,从⽽决定后续动
作。
需要注意的是,在接收到服务器的回调通知后,根据通知的result_code字段判断⽀付是否成功。在接受到成功的通知后,后端需要返回success数据向服务器告知已得
到回调通知。否则服务器会不停的向后端发送消息。另外的通知是以XML格式发送的,在接受处理时需要注意。
的⼤概⽀付流程就是这样。以下是PHP语法的⽀付类,可以⽐照上⾯的步骤介绍,加深理解。在需要⽀付时,直接传⼊参数实例化此类再调⽤类的 pay ⽅法即可。
//⽀付类
class WeiXinPay{
//=======【基本信息设置】=====================================
//⾝份的唯⼀标识
protected $APPID = appid;//填写您的appid。公众平台⾥的
protected $APPSECRET = secret;
//受理商ID,⾝份标识
protected $MCHID = '11111111';//商户id
//商户⽀付密钥Key
protected $KEY = '192006250b4c09247ec02edce69f6a2d';
//回调通知接⼝
protected $APPURL =      'smart.afei/receivesuc';
//交易类型
protected $TRADETYPE = 'JSAPI';
//商品类型信息
protected $BODY = 'wx/book';
//⽀付类的构造函数
function __construct($openid,$outTradeNo,$totalFee){
$this->openid = $openid; //⽤户唯⼀标识
$this->outTradeNo = $outTradeNo; //商品编号
$this->totalFee = $totalFee; //总价
}
/
/⽀付类向外暴露的⽀付接⼝
public function pay(){
$result = $this->weixinapp();
return $result;
}
//对统⼀下单接⼝返回的⽀付相关数据进⾏处理
private function weixinapp(){
$unifiedorder=$this->unifiedorder();
$parameters=array(
'appId'=>$this->APPID,//⼩程序ID
'timeStamp'=>''.time().'',//时间戳
'nonceStr'=>$this->createNoncestr(),//随机串
'package'=>'prepay_id='.$unifiedorder['prepay_id'],//数据包
'signType'=>'MD5'//签名⽅式
);
$parameters['paySign']=$this->getSign($parameters);
return $parameters;
}
/*
*请求统⼀下单接⼝
*/
private function unifiedorder(){
$parameters = array(
'appid' => $this->APPID,//⼩程序id
'mch_id'=> $this->MCHID,//商户id
'spbill_create_ip'=>$_SERVER['REMOTE_ADDR'],//终端ip
'notify_url'=>$this->APPURL, //通知地址
'nonce_str'=> $this->createNoncestr(),//随机字符串
'out_trade_no'=>$this->outTradeNo,//商户订单编号
'total_fee'=>floatval($this->totalFee), //总⾦额
'open_id'=>$this->openid,//⽤户openid
'trade_type'=>$this->TRADETYPE,//交易类型
'body' =>$this->BODY, //商品信息
)
;
$parameters['sign'] = $this->getSign($parameters);
$xmlData = $this->arrayToXml($parameters);
$xml_result = $this->postXmlCurl($xmlData,'h.weixin.qq/pay/unifiedorder',60);
$result = $this->xmlToArray($xml_result);
return $result;
}
//数组转字符串⽅法
protected function arrayToXml($arr){
$xml = "<xml>";
foreach ($arr as $key=>$val)
{
if (is_numeric($val)){
$xml.="<".$key.">".$val."</".$key.">";
}else{
$xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
}
}
$xml.="</xml>";
return $xml;
}
protected function xmlToArray($xml){
$array_data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);        return $array_data;
}
//发送xml请求⽅法
private static function postXmlCurl($xml, $url, $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, FALSE); //严格校验
//设置header
curl_setopt($ch, CURLOPT_HEADER, FALSE);
//要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
//post提交⽅式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($ch, CURLOPT_TIMEOUT, 40);
set_time_limit(0);
//运⾏curl
$data = curl_exec($ch);
//返回结果
if ($data) {
curl_close($ch);
return $data;
} else {
$error = curl_errno($ch);
curl_close($ch);
throw new WxPayException("curl出错,错误码:$error");
}
}
/
*
* 对要发送到统⼀下单接⼝的数据进⾏签名
*/
protected function getSign($Obj){
foreach ($Obj as $k => $v){
$Parameters[$k] = $v;
}
//签名步骤⼀:按字典序排序参数
ksort($Parameters);
$String = $this->formatBizQueryParaMap($Parameters, false);
//签名步骤⼆:在string后加⼊KEY
$String = $String."&key=".$this->KEY;
//签名步骤三:MD5加密
$String = md5($String);
//签名步骤四:所有字符转为⼤写
$result_ = strtoupper($String);
return $result_;
}
/*
*排序并格式化参数⽅法,签名时需要使⽤
*/
protected function formatBizQueryParaMap($paraMap, $urlencode)
{
$buff = "";
ksort($paraMap);
foreach ($paraMap as $k => $v)
{
if($urlencode)
{
$v = urlencode($v);
}
//$buff .= strtolower($k) . "=" . $v . "&";
$buff .= $k . "=" . $v . "&";
}
$reqPar;
if (strlen($buff) > 0)
{
$reqPar = substr($buff, 0, strlen($buff)-1);
}
return $reqPar;
}
/*
* ⽣成随机字符串⽅法
*/
protected function createNoncestr($length = 32 ){
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str ="";
for ( $i = 0; $i < $length; $i++ ) {
$str.= substr($chars, mt_rand(0, strlen($chars)-1), 1);
}
return $str;
}
}
以上就是⽀付的相关流程。在理清思路后,流程还是⽐较清晰和简单的。重点在于需要注意⼀些细节问题,例如数据格式,加密⽅法等。
下⾯说⼀下⼩程序退款的具体实现
⼆.退款
⼩程序退款的流程和付款相似,但有⼀些细节上的不同。
⾸先退款的步骤通常如下:
1. ⽤户前端点击退款按钮后,后端接收到⽤户的退款请求通过商城后台呈现给商户,商户确定允许退款后,后端再发起向退款接⼝的请求来请求退款。
2. 后端向退款接⼝发送请求后,得到响应信息,确定退款是否完成,根据退款是否完成再去进⾏改变订单状态等业务逻辑。
退款的步骤相对⽀付来说⽐较简单。
值得注意的有以下两点:
1.向退款接⼝请求退款后,根据得到的响应是可以直接确定退款是否完成的。不再需要设置专门的回调接⼝等待通知。当然如果需要也是可以在商户平台设置回调
接⼝接受从⽽接受回调的,但并不是必须的。
2.退款请求需要在请求服务器安装提供的安全证书,也就是说,发起退款请求相⽐较⽀付请求在请求时请求⽅法不能复⽤,因为退款需要携带证书的请求,此证书可在申请
商户号成功后从商户平台⾃⾏下载,Linux下的PHP开发环境的证书只需要放在⽹站根⽬录的cert⽂件夹中即可。其他开发环境可能需要导⼊操作。
下⾯讲解⼀下退款的具体步骤
⼀. ⽤户发起退款请求
⽤户在前端发起退款请求,后端接收到退款请求,将相应订单标记为申请退款,展⽰在后台.商户查看后,如果同意退款再进⾏相应操作.此后才进⼊真正的退款流程.
⼆. 商户发起退款请求
商户同意退款后,后端即向提供的退款 API 发起请求.
同请求⽀付API⼀样.退款请求也需要将需要的参数进⾏签名后以XML发送到的退款API [h.weixin.qq/pay/refund](h.weixin.qq/pay/refund)
退款请求需要的参数如下(多个参数在⽀付API请求时也有使⽤):
1. ⼩程序 appid。
2. 商户号 mch_id 。申请开通⽀付商户认证成功后发给你的邮件⾥有
3. 商户订单号 out_trade_no 。退款订单在⽀付时⽣成的订单号
4. 退款订单号 out_refund_no 。由后端⽣成的退款单号,需要保证唯⼀,因为多个同样的退款单号只会退款⼀次。
5. 总⾦额 total_fee 。订单总⾦额,单位为分。
6. 退款⾦额 refund_fee 需要退款的⾦额,单位同样为分
7. 操作员 op_user_id .与商户号相同即可
8. 随机字符串 nonce_str 。同⽀付请求
定制小程序需要多少钱?9. 签名 sign 。使⽤上⾯的所有参数进⾏相应处理加密⽣成签名。(具体处理⽅式与⽀付相同,可直接复⽤。)
三. 退款完成
在发起退款请求后,就可以直接根据请求的响应XML中的 result_code字段来判断退款是否成功,从⽽对订单状态进⾏处理和后续操作。不需要像⽀付那样等待另⼀个接⼝的通知来确定请求状态。当然如上⽂所说,如果需要服务器发送通知到
退款因为流程与⽀付⼤同⼩异,因此退款的PHP类我选择了直接继承⽀付类,
代码如下,注意区分退款请求⽅法postXmlSSLCurl和⽀付请求⽅法postXmlCurl的区别,这也就是上⽂提到的退款需要的双向证书的使⽤。
class WinXinRefund extends WeiXinPay{
protected \$SSLCERT_PATH = 'cert/apiclient_cert.pem';//证书路径
protected \$SSLKEY_PATH =  'cert/apiclient_key.pem';//证书路径
protected \$opUserId = '1234567899';//商户号
function __construct($openid,$outTradeNo,$totalFee,$outRefundNo,$refundFee){
//初始化退款类需要的变量
$this->openid = $openid;
$this->outTradeNo = $outTradeNo;
$this->totalFee = $totalFee;
$this->outRefundNo = $outRefundNo;
$this->refundFee = $refundFee;
}
public function refund(){
//对外暴露的退款接⼝
$result = $this->wxrefundapi();
return $result;
}
private function wxrefundapi(){
//通过api进⾏退款流程
$parma = array(
'appid'=> $this->APPID,
'mch_id'=> $this->MCHID,
'nonce_str'=> $this->createNoncestr(),
'out_refund_no'=> $this->outRefundNo,
'out_trade_no'=> $this->outTradeNo,
'total_fee'=> $this->totalFee,
'refund_fee'=> $this->refundFee,
'op_user_id' => $this->opUserId,
)
;
$parma['sign'] = $this->getSign($parma);
$xmldata = $this->arrayToXml($parma);
$xmlresult = $this->postXmlSSLCurl($xmldata,'h.weixin.qq/secapi/pay/refund');
$result = $this->xmlToArray($xmlresult);
return $result;
}
//需要使⽤证书的请求
function postXmlSSLCurl($xml,$url,$second=30)
{
$ch = curl_init();
/
/超时时间
curl_setopt($ch,CURLOPT_TIMEOUT,$second);
//这⾥设置代理,如果有的话
//curl_setopt($ch,CURLOPT_PROXY, '8.8.8.8');
//curl_setopt($ch,CURLOPT_PROXYPORT, 8080);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);
//设置header
curl_setopt($ch,CURLOPT_HEADER,FALSE);
//要求结果为字符串且输出到屏幕上
curl_setopt($ch,CURLOPT_RETURNTRANSFER,TRUE);
//设置证书
//使⽤证书:cert 与 key 分别属于两个.pem⽂件
//默认格式为PEM,可以注释
curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLCERT, $this->SSLCERT_PATH);
//默认格式为PEM,可以注释
curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLKEY, $this->SSLKEY_PATH);
//post提交⽅式
curl_setopt($ch,CURLOPT_POST, true);
curl_setopt($ch,CURLOPT_POSTFIELDS,$xml);
$data = curl_exec($ch);
//返回结果
if($data){
curl_close($ch);
return $data;
}
else {
$error = curl_errno($ch);
echo "curl出错,错误码:$error"."<br>";
curl_close($ch);
return false;
}
}}
## 三. 总结
以上就是关于⽀付和退款的流程及相关知识的介绍。⽂中的 PHP类均封装直接可⽤。
因为⽀付和退款涉及的东西较为繁杂,很多⼈直接看官⽅⽂档可能会⼀头雾⽔,所以看过此⽂了解流程和要点后,再去看官⽅⽂档。⼀⽅⾯可以更清晰的了解⼩程序的⽀付和退款流程。另⼀⽅⾯,本⽂因为篇幅有限及作者能⼒有限,肯定最后扯点闲话吧。这篇博客本来应该在三个⽉前就发表的,也算当时我从⼀⽆所知到独⽴完成⼩程序商城前后端的总结系列的第⼀篇。但是公司突然出现⼈员和项⽬的变动,导致管理和项⽬上都混乱不堪,再加上个⼈的惰性,导致此篇博客⼀借⽤李志的⼀句歌词结束这篇博客吧。下⼀篇是什么时候也说不定了,我苦笑。
>我再也不会把⾃⼰,愚蠢的交给过去。我的⽣活和我的想法,从此相隔万⾥。