♦先看本节效果图
详细说明请看微信支付官方开发文档:
1. 用户在小程序内下单,选择微信支付;
2. 商户在小程序中调用小程序登录API,获得参数code;
3. 小程序端向商户后台发起接口调用,并将code及订单相关参数一起发送到商户后台。
4. 商户后台接收小程序发送的code和订单相关参数,并结合appid,secret两个参数,获取openid;
5. 商户后台根据订单信息,调用统一下单接口;
6. 统一下单接口返回预支付信息,商户后台获取预支付信息,并进行再次签名,返回支付参数(5个参数和sign)给小程序;
7. 小程序获得支付参数,发起支付请求;
8. 用户输入支付密码,支付完成;
9. 微信后台向商户后台发出异步通知,同时给小程序回调支付结果;
10.商户后台接收微信发送到异步通知,并进行相关业务处理,并返回SUCCESS或FAIL的标志以告知微信;
11.小程序获取支付回调结果,并向商户后台发起接口请求,获取订单状态;并进行支付成功或失败对应的页面跳转。
微信小程序支付文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_3&index=1
业务流程图
♦核心代码就下面这些
♦新建Pay.php文件
//支付接口
class Pay {
/*
* 小程序微信支付*/
protected $appid;
protected $mch_id;
protected $key;
protected $openid;
protected $out_trade_no;
protected $body;
protected $total_fee;
function __construct($appid, $openid, $mch_id, $key,$out_trade_no,$body,$total_fee) {
$this->appid = $appid;
$this->openid = $openid;
$this->mch_id = $mch_id;
$this->key = $key;
$this->out_trade_no = $out_trade_no;
$this->body = $body;
$this->total_fee = $total_fee;
}
public function pay() {
//统一下单接口
$return = $this->weixinapp();
return $return;
}
//统一下单接口
private function unifiedorder() {
$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
$parameters = array(
'appid' => $this->appid, //小程序ID
'mch_id' => $this->mch_id, //商户号
'nonce_str' => $this->createNoncestr(), //随机字符串
'body' => $this->body,
'out_trade_no'=> $this->out_trade_no,
//'total_fee' => floatval(0.01 * 100), //总金额 单位 分
'total_fee' => $this->total_fee,
'spbill_create_ip' => $_SERVER['REMOTE_ADDR'], //终端IP
'notify_url' => "https://" . $_SERVER['HTTP_HOST'] . "/public/api.php/client_api/pay2/payNotify",//通知地址
'openid' => $this->openid, //用户id
'trade_type' => 'JSAPI'//交易类型
);
//统一下单签名
$parameters['sign'] = $this->getSign($parameters);
$xmlData = $this->arrayToXml($parameters);
$return = $this->xmlToArray($this->postXmlCurl($xmlData, $url, 60));
return $return;
}
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");
}
}
//数组转换成xml
private function arrayToXml($arr) {
$xml = "
foreach ($arr as $key => $val) {
if (is_array($val)) {
$xml .= "<" . $key . ">" . arrayToXml($val) . "" . $key . ">";
} else {
$xml .= "<" . $key . ">" . $val . "" . $key . ">";
}
}
$xml .= "";
return $xml;
}
//xml转换成数组
private function xmlToArray($xml) {
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$xmlstring = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
$val = json_decode(json_encode($xmlstring), true);
return $val;
}
//微信小程序接口
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;
}
//作用:产生随机字符串,不长于32位
private 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;
}
//作用:生成签名
private 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_;
}
///作用:格式化参数,签名过程需要使用
private function formatBizQueryParaMap($paraMap, $urlencode) {
$buff = "";
ksort($paraMap);
foreach ($paraMap as $k => $v) {
if ($urlencode) {
$v = urlencode($v);
}
$buff .= $k . "=" . $v . "&";
}
$reqPar;
if (strlen($buff) > 0) {
$reqPar = substr($buff, 0, strlen($buff) - 1);
}
return $reqPar;
}
♦新建接口文件Pay2.php
♦在这个文件里面调用Pay.php,开始使用
class Pay2{
//支付费用
public function payJoinfee(){
$data = request()->param();
$appid='小程序的APPID';
$openid=$data['openid'];
$mch_id='商户号';
$key='设置的公众号token';
$out_trade_no = $mch_id.time();
$row = Db::table('dp_admin_per')->find($data['per_id']);
$arr['order_sn'] = $out_trade_no;
$arr['uid'] = $data['uid'];
$arr['atime'] = time();
$arr['name'] = $row['name'];
$arr['price'] = $row['value'] * 365;
$arr['openid'] = $openid;
$arr['per_id'] = $data['per_id'];
require "Pay.php";
$weixinpay = new Pay($appid,$openid,$mch_id,$key,$arr['order_sn'],$arr['name'],$arr['price']);
$return=$weixinpay->pay();
Db::table('order')->insert($arr);
return json(['code'=>200,'msg'=>'获取成功','res'=>$return]);
}
/**
* 支付回调
* @author:大脸猫脸大
*/
public function payNotify()
{
//接收微信返回的数据数据,返回的xml格式
$xmlData = file_get_contents('php://input');
//将xml格式转换为数组
$data = $this->FromXml($xmlData);
//用日志记录检查数据是否接受成功,验证成功一次之后,可删除。
$path_dir='./log.txt';
if (!is_dir(dirname($path_dir))) {
mkdir($path_dir, 0777, true);
}
$file = fopen('./log.txt', 'a+');
fwrite($file,var_export($data,true));
//为了防止假数据,验证签名是否和返回的一样。
//记录一下,返回回来的签名,生成签名的时候,必须剔除sign字段。
$sign = $data['sign'];
unset($data['sign']);
if($sign == $this->getSign($data)){
//签名验证成功后,判断返回微信返回的
if ($data['result_code'] == 'SUCCESS') {
//根据返回的订单号做业务逻辑
$re = Db::table('order')->where(['order_sn'=>$data['out_trade_no']])->setField(['order_status'=>1,'pay_time'=>time()]);
$sql = Db::table('order')->getLastSql();
$file = fopen('./log.txt', 'a+');
fwrite($file,"订单状态修改成功:".$sql."\r\n");
//处理完成之后,告诉微信成功结果!
//增加会员到期时间
$row = Db::table('order')->where('order_sn','15642106611574737298')->find();
$vip_end_time = Db::table('dp_admin_user')->where('id',$row['uid'])->value('vip_end_time');
if ($vip_end_time || $vip_end_time != 0){
$vip_end_time = (time() +(365 * 86400)) + ($vip_end_time - time());
}else{
$vip_end_time = time() +(365 * 86400);
}
Db::table('dp_admin_user')->where('id',$row['uid'])->setInc(['vip_end_time'=>$vip_end_time]);
Db::table('dp_admin_user')->where('id',$row['uid'])->setField(['is_vip'=>1]);
if($re){
echo '
';exit();
}
}
//支付失败,输出错误信息
else{
$file = fopen('./log.txt', 'a+');
fwrite($file,"错误信息:".$data['return_msg'].date("Y-m-d H:i:s"),time()."\r\n");
}
}
else{
$file = fopen('./log.txt', 'a+');
fwrite($file,"错误信息:签名验证失败".date("Y-m-d H:i:s"),time()."\r\n");
}
}
private function getSign($params) {
ksort($params); //将参数数组按照参数名ASCII码从小到大排序
foreach ($params as $key => $item) {
if (!empty($item)) { //剔除参数值为空的参数
$newArr[] = $key.'='.$item; // 整合新的参数数组
}
}
$stringA = implode("&", $newArr); //使用 & 符号连接参数
$stringSignTemp = $stringA."&key=设置的公众号token♥"; //拼接key
// key是在商户平台API安全里自己设置的
$stringSignTemp = MD5($stringSignTemp); //将字符串进行MD5加密
$sign = strtoupper($stringSignTemp); //将所有字符转换为大写
return $sign;
}
//将xml数据转换为数组,接收微信返回数据时用到.
public function FromXml($xml)
{
if(!$xml){
echo "xml数据异常!";
}
//将XML转为array
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
return $data;
}
}
}