背景事先准备工作申请一个小程序,并开通微信支付,详细见:微信小程序支付业务说明仔细查阅微信支付官方文档,详细见: 微信支付开发者文档仔细阅读 微信支付统一下单接口仔细阅读 支付结果通知接口整理并在商户平台设置好相应的回掉地址,比如http://test.dev.com/wechat/pa…服务端编写两个接口 1) 微信预支付接口,http://test.dev.com/wechat/pr… , 以商品订单为例,此接口接受两个参数,goods_id 和 uid ,goods_id表示商品编号,uid表示用户编号,返回参数如下 { errcode: 200, msg: “SUCCESS”, data: { status: 1, //状态,为1表示成功,其他的表示失败 result: “success”, data: { appId: “xxx”, //小程序的appid timeStamp: 1545909092, //时间戳 nonceStr: “vuryhptlafvpee92pxhji6zs5jl2n0gu”, //随机串 package: “prepay_id=wx27191130962951f060bfa1323531879649”, //支付的包参数 signType: “MD5”, //签名方式 paySign: “B04272BB9BBDB1F52863D3B0EF580BE8” //支付签名 } } }2) 微信支付回调接口,http://test.dev.com/wechat/pa… ,此接口最好是get和post都设置,因为 微信在进行回调的时候会以post的形式进行请求5.建表 1) 商品订单表(shop_goods_order),其中重要的字段有out_trade_no,out_trade_no传递给微信支付的支付订单号,也是我们自己的系统与微信对接的订单唯一标识;bill_no表示微信支付的交易订单号,这个字段只有在订单支付成功之后进行更新,该字段也是查询位置支付订单的唯一标识,详细的表结构如下CREATE TABLE shop_goods_order ( id int(10) NOT NULL AUTO_INCREMENT, uid int(10) DEFAULT ‘0’ COMMENT ‘用户编号’, goods_id int(10) DEFAULT ‘0’ COMMENT ‘商品编号’, out_trade_no varchar(30) DEFAULT ’’ COMMENT ‘订单序列号’, bill_no varchar(30) DEFAULT ’’ COMMENT ‘支付方返回的交易订单号’, paid_money int(10) DEFAULT ‘0’ COMMENT ‘支付的金额’, paid_integral int(10) DEFAULT ‘0’ COMMENT ‘支付的健康币’, paid_type varchar(15) DEFAULT ‘WXPAY’ COMMENT ‘支付类型,有WXPAY和INTEGRAL等值’, paid_status varchar(10) DEFAULT ‘CHECKED’ COMMENT ‘支付状态,CHECKED表示初始状态,SUCC表示支付成功,FAILED表示支付失败,REFUND表示已退款’, add_time int(10) DEFAULT ‘0’ COMMENT ‘添加时间’, paid_time int(10) DEFAULT ‘0’ COMMENT ‘支付时间’, update_time int(10) DEFAULT ‘0’ COMMENT ‘更新时间’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;2) 商品信息表(shop_goods_info),字段如下CREATE TABLE shop_goods_info ( id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, name varchar(100) DEFAULT ’’ COMMENT ‘商品名称’, note varchar(300) DEFAULT ’’ COMMENT ‘商品描述’, market_price int(10) DEFAULT ‘0’ COMMENT ‘原价’, sale_price int(10) DEFAULT ‘0’ COMMENT ‘售价’, integral int(8) DEFAULT ‘0’ COMMENT ‘健康币’, main_thumbnail varchar(40) DEFAULT ’’ COMMENT ‘主图’, thumbnail1 varchar(40) DEFAULT ’’ COMMENT ‘缩略图1’, thumbnail2 varchar(40) DEFAULT ’’ COMMENT ‘缩略图2’, thumbnail3 varchar(40) DEFAULT ’’ COMMENT ‘缩略图3’, thumbnail4 varchar(40) DEFAULT ’’ COMMENT ‘缩略图4’, thumbnail5 varchar(40) DEFAULT ’’ COMMENT ‘缩略图5’, content text COMMENT ‘详细介绍’, add_time int(10) DEFAULT ‘0’ COMMENT ‘添加时间’, update_time int(10) DEFAULT ‘0’ COMMENT ‘更新时间’, is_online tinyint(1) DEFAULT ‘1’ COMMENT ‘商品是否上线’, sort int(4) DEFAULT ‘0’ COMMENT ‘排序值,越大越靠前’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;实现步骤业务实现时序图实现步骤说明客户端客户端调用 微信预支付接口 获取对应的微信支付参数获取基础的支付参数后,调用wx.requetPayment接口调起微信支付用户输入密码完成支付服务端客户端在发起支付前先往商品订单表里面创建一条订单,并生成对应的out_trade_no参数调用微信支付的统一下单接口https://api.mch.weixin.qq.com…,向微信发起支付订单请求,统一下单接口文档地址,见微信支付统一下单接口支付请求结束后微信将支付结果返回给 微信支付回调接口若支付成功,服务端将订单的paid_status字段设置succ ,并将bill_no、paid_time、update_time更新,bill_no的值为微信支付的transaction_id;若支付失败,将paid_status字段更新为failed,并更新update_time字段关键代码客户端发起微信支付 wxPay:function () { var that = this var params = { goods_id: that.data.goods_id, uid: that.data.uid, paid_type: ‘WXPAY’ } var param = JSON.stringify(params) console.log(param) param = app.Encrypt(param) var url = app.data.API_DOMAIN + “/wechat/prepay?param=” + param wx.showModal({ title: ‘提示’, content: ‘确定要微信支付购买此系列课吗?’, success(res) { if (res.confirm) { if (that.data.iswxpay == 0) { that.setData({ iswxpay: 1 }) app.httpRequest(that.data.uid, url, function (response) { var payinfo = response.data.data.data wx.requestPayment({ timeStamp: payinfo.timeStamp.toString(), nonceStr: payinfo.nonceStr, package: payinfo.package, signType: ‘MD5’, paySign: payinfo.paySign, success(res) { wx.showToast({ title: ‘购买成功’, icon: ‘success’ }) that.setData({ is_paid: 1 }) that.getSeminarInfo(that.data.sid, that.data.uid) }, fail(res) { that.setData({ iswxpay: 0 }) wx.showToast({ title: ‘购买失败’, icon: ’none’ }) } }) console.log(response.data.data.data) }, function (f_res) { }, function (f_res) { }) } } else { that.setData({ iswxpay: 0 }) console.log(‘取消微信支付’) } } }) },服务端预支付接口关键代码1、入口方法:orderPay/** * 微信支付的获取支付参数的接口 * 1.先要用户编号和支付方式获取对应的订单,如果存在则取存在的,若不存在则创建,一种支付类型的订单值存在一条记录 * 2.创建订单后根据out_trade_no来调用微信支付的统一下单接口得到微信支付的支付参数 * 3.将参数返回给前端进行支付 * 4.支付成功之后进行回掉 / public function orderPay($uid, $goodsId, $paidType){ $result = []; $lockKey = BusinessHelper::getPayOrderLockRedisKey(); //枷锁是为了防止并发 $this->doWithLock(function()use(&$result,$uid,$goodsId,$paidType){ error_log(’$paidType ================>’.$paidType); switch ($paidType){ case Constant::PAID_TYPE_MIXED : error_log(‘doIntegralPay ================>’); $result = $this->doMixedPay($uid,$goodsId,$paidType); error_log(‘integral pay result ================>’.json_encode($result)); break; case Constant::PAID_TYPE_WXPAY : $result = $this->doWxaPay($uid,$goodsId,$paidType); error_log(‘wx pay result ================>’.json_encode($result)); break; } },$lockKey,5); error_log(‘result ================>’.json_encode($result)); return $result; }2、微信核心支付方法:doWxaPay/* * 通过小程序支付的逻辑 * @param $uid 用户编号 * @param $goodsId 系列课编号 * @param $paidType 支付类型,有INTEGRAL和WXPAY两种 * @return array / public function doWxaPay($uid, $goodsId, $paidType){ $goodsInfo = ShopGoodsInfoService::getById($goodsId); if(!$goodsInfo){ return [ ‘status’ => -1, ‘result’ => ‘商品已经下架或者不存在’ ]; } $config = BusinessHelper::getWechatPayConfig(); $payHelper = new WechatPayHelper($config); $payContent = $this->getWxaPrepayContent($uid,$paidType,$goodsId); $params = $payHelper->prepay($payContent); error_log(‘param ==============>’.json_encode($params)); return $params; }3、创建订单方法:createOrder这个方法是为了建立订单,为了保证表示每一次支付都建立一个订单,我这边做两重的订单复用,先根据订单状态去查询是否有待支付的订单,如果有在判断这个订单的差功能键时间是否已经超过7天,如果超过七天则另外创建新的订单,尽最大的进行数据复用/* * 创建和验证订单,接口方法 * @param $uid 用户编号 * @param $paidType 支付类型 * @param $goodsId 系列课编号 * @return array / protected function createOrder($uid, $paidType, $goodsId){ $existOrder = $this->getUserGoodsOrderWithPaidType($uid,$paidType,$goodsId); if(!$existOrder){ return $this->generateOrder($uid,$paidType,$goodsId); } //验证7天之类订单有效 $createTime = date(‘Y-m-d’,$existOrder[‘add_time’]); $today = date(‘Y-m-d’); $diff = TimeHelper::getDiffBetweenTwoDays($today,$createTime); if($diff > 7){ return $this->generateOrder($uid,$paidType,$goodsId); } return $existOrder; }4、订单查重方法:getUserGoodsOrderWithPaidType /* * 根据支付类型获取用户对应的商品的订单 / public function getUserGoodsOrderWithPaidType($uid, $paidType, $goodsId){ $order = BeanHelper::convertStdClsToArr( ShopGoodsOrder::where(‘uid’, $uid) ->where(‘goods_id’,$goodsId) ->where(‘paid_type’,$paidType) ->whereIn(‘paid_status’,[Constant::PAID_STATUS_CHECKED]) ->orderBy(‘add_time’,‘desc’) ->first() ); return $order; }5、生成订单方法:/* * 生成订单,辅助方法 * @param $uid 用户编号 * @param $paidType 支付类型 * @param $goodsId 系列课编号 * @return array / public function generateOrder($uid, $paidType, $goodsId){ $goodsInfo = ShopGoodsInfoService::getById($goodsId); $priceKey = $paidType == Constant::PAID_TYPE_WXPAY ? ‘market_price’ : ‘sale_price’; $price = formatArrValue($goodsInfo,$priceKey,0); $integral = $paidType == Constant::PAID_TYPE_WXPAY ? 0 : formatArrValue($goodsInfo,‘integral’,0); $baseMeasureUnit = 100; $insertOrderData = [ ‘uid’ => $uid, ‘goods_id’ => $goodsId, ‘out_trade_no’ => BusinessHelper::generateOutTradeNo(Constant::PAID_SCENE_SHOP_GOODS_ORDER), ‘paid_money’ => $price * $baseMeasureUnit, ‘paid_integral’ => $integral, ‘paid_type’ => $paidType, ‘paid_status’ => Constant::PAID_STATUS_CHECKED, ‘add_time’ => time(), ‘update_time’ => time(), ]; $existOrder = BeanHelper::convertStdClsToArr($this->store($insertOrderData)); return $existOrder; }6、生成outTradeNo方法这个方法中的getPaidSceneMapping方法返回的是一个数组,out_trade_no方法有3个部分组成,分别是当前时间,场景值(这个是为了保证不同的支付场景对应的不同的业务代码)以及10位随机数字组成 /* * 生成第三方支付的外部订单号 / public static function generateOutTradeNo($paidScene = Constant::PAID_SCENE_SEMINAR_ORDER){ $prefix = date(‘YmdHis’); $paidSceneMap = self::getPaidSceneMapping(); $scene = formatArrValue($paidSceneMap,$paidScene,‘0001’); $suffix = generateRandomNum(10); return $prefix.$scene.$suffix; } /* * 获取支付场景的map,这个是为了区分不同的支付场景时候更新不同的业务字段,为了拓展进行的预留 / public static function getPaidSceneMapping(){ return [ Constant::PAID_SCENE_SEMINAR_ORDER => ‘0001’, Constant::PAID_SCENE_SHOP_GOODS_ORDER => ‘0002’ ]; }支付回调接口关键代码入口方法:payNotify/* * 支付的回掉 /public function payNotify(Request $request){ error_log(’notify request param ========>’); $config = BusinessHelper::getWechatPayConfig(); $helper = new WechatPayHelper($config); $result = $helper->notify($request); return $result;}微信支付帮助类<?phpnamespace App\Http\Helper\Pay;use App\Http\Helper\Jz\BusinessHelper;use App\Http\Helper\Jz\Constant;use App\Http\Helper\LogHelper;use SeminarOrderService;use ShopGoodsOrderService;/* * Created by PhpStorm. * User: Auser * Date: 2018/12/17 * Time: 15:41 /class WechatPayHelper { public $config; public function __construct($config) { $this->config = $config; } /* * 预支付请求接口(POST) * 返回json的数据 / public function prepay($payContent) { $config = $this->config; $unifiedorder = [ ‘appid’ =>$config[‘appid’], ‘mch_id’ =>$config[‘mchid’], ’nonce_str’ =>self::getNonceStr(), ‘body’ =>$payContent[‘body’], ‘out_trade_no’ =>$payContent[‘out_trade_no’], ’total_fee’ =>$payContent[‘fee’], ‘spbill_create_ip’=>$_SERVER[‘REMOTE_ADDR’], ’notify_url’ =>$config[’notify_url’], ’trade_type’ =>‘JSAPI’, ‘openid’ =>$payContent[‘openid’] ]; error_log(‘config ===============>’.json_encode($config)); $unifiedorder[‘sign’] = $this->makeSign($unifiedorder); error_log(‘unifine order param ===============>’.json_encode($unifiedorder)); //请求数据 $xmldata = $this->array2xml($unifiedorder); $url = ‘https://api.mch.weixin.qq.com/pay/unifiedorder'; $res = $this->request($url, $xmldata); if(!$res){ return $this->errorResult(“Can’t connect the server”); } $content = $this->xml2array($res); error_log(‘unifine order result ===============>’.json_encode($content)); if(strval($content[‘result_code’]) == ‘FAIL’){ return $this->errorResult(strval($content[‘return_msg’])); } if(strval($content[‘return_code’]) == ‘FAIL’){ return $this->errorResult(strval($content[‘return_msg’])); } //拼接小程序的接口数据 $resData = [ ‘appId’ => strval($content[‘appid’]), ’timeStamp’ => time(), ’nonceStr’ => $this->getNonceStr(), ‘package’ => ‘prepay_id=’.strval($content[‘prepay_id’]), ‘signType’ => ‘MD5’ ]; //加密签名 $resData[‘paySign’] = $this->makeSign($resData); return $this->successResult($resData); } /* * @return array|bool * 微信支付回调验证 * 返回数据 / public function notify(){ //$xml = $GLOBALS[‘HTTP_RAW_POST_DATA’]; error_log(“wechat pay notify message ============>”); $xml = file_get_contents(‘php://input’); //将服务器返回的XML数据转化为数组 $data = $this->xml2array($xml); // 保存微信服务器返回的签名sign $dataSign = $data[‘sign’]; // sign不参与签名算法 unset($data[‘sign’]); $sign = $this->makeSign($data); // 判断签名是否正确 判断支付状态 $result = false; error_log(“return data ============>".json_encode($data)); //验证订单是否已经支付,调用订单查询接口 $isPayment = $this->verifyPament($data); error_log(“isPayment ============>”.$isPayment); if($isPayment && ($data[‘return_code’]==‘SUCCESS’) && ($data[‘result_code’]==‘SUCCESS’)) { error_log(“isPayment success============>”); $outTradeNo = $data[‘out_trade_no’]; $concurrentTime = 30; $lockKey = getCacheKey(‘redis_key.cache_key.zset_list.lock’) . $outTradeNo; //采用并发锁控制并发 SeminarOrderService::doWithLock(function()use(&$result , $data){ $result = $data; $this->setPaidSuccess($data); },$lockKey,$concurrentTime); }else{ error_log(“isPayment failed============>”); $this->setPaidFail($data); } // 返回状态给微信服务器 if($result){ $str=’<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>’; }else { $str=’<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>’; } return $str; } /* * 支付成功 / public function setPaidSuccess($data){ error_log(‘current paid data =============>’.json_encode($data)); $paidType = substr($data[‘out_trade_no’], 14, 4); error_log(‘current paid type is =============>’.$paidType); switch ($paidType){ case ‘0001’ : SeminarOrderService::setOrderPaid($data); break; case ‘0002’: ShopGoodsOrderService::setOrderPaid($data); break; } } /* * 支付失败 / public function setPaidFail($data){ $paidType = intval(substr($data[‘out_trade_no’], 14, 4)); LogHelper::info(‘current paid type is =============>’.$paidType); switch ($paidType){ case ‘0001’ : SeminarOrderService::setOrderPaidFailed($data); break; case ‘0002’: ShopGoodsOrderService::setOrderPaidFailed($data); break; } } /* * 验证支付的问题 / public function verifyPament($wxPayResp){ error_log(“verify paymnent method=======>".json_encode($wxPayResp)); $url = “https://api.mch.weixin.qq.com/pay/orderquery"; //检测必填参数 if(!$wxPayResp[’transaction_id’] && !$wxPayResp[‘out_trade_no’]) { error_log(“订单查询接口中,out_trade_no、transaction_id至少填一个!”); return false; } error_log(“开始查询==============》接口”); $config = BusinessHelper::getWechatPayConfig(); error_log(“post config ==============》".json_encode($config)); error_log(“transaction is===============>”.$wxPayResp[’transaction_id’]); error_log(“appid is===============>”.$config[‘appid’]); error_log(“transaction is===============>”.$config[‘mchid’]); error_log(“nonce_string is===============>”.$this->getNonceStr()); $params = [ ‘appid’ => $config[‘appid’], ‘mch_id’ => $config[‘mchid’], ’nonce_str’ => $this->getNonceStr(), ’transaction_id’ => $wxPayResp[’transaction_id’] ]; error_log(“post PARAM without sign==============》”); $params[‘sign’] = $this->makeSign($params); error_log(“post PARAM0 with sign ==============》”); $xmlData = $this->array2xml($params); $response = $this->request($url,$xmlData); if(!$response){ error_log(“接口请求错误:”); return false; } $result = $this->xml2array($response); error_log(“查询订单接口返回结果:".json_encode($result)); if(array_key_exists(“return_code”, $result) && array_key_exists(“trade_state”, $result) && $result[“return_code”] == “SUCCESS” && $result[“trade_state”] == “SUCCESS”){ return true; } return false; }//—————————————————————用到的函数———————————————————— /* * 错误返回提示 * @param string $errMsg 错误信息 * @param string $status 错误码 * @return array json的数据 / protected function errorResult($errMsg = ’error’, $status = Constant::PAID_RESULT_FAILED) { return [ ‘status’=>$status, ‘result’=>‘fail’, ‘data’=>$errMsg ]; } /* * 正确返回 * @param array $data 要返回的数组 * @return array json的数据 / protected function successResult($data=[]){ return [ ‘status’=> Constant::PAID_RESULT_SUCCESS, ‘result’=>‘success’, ‘data’=>$data ]; } /* * 将一个数组转换为 XML 结构的字符串 * @param array $arr 要转换的数组 * @param int $level 节点层级, 1 为 Root. * @return string XML 结构的字符串 / protected function array2xml($arr, $level = 1){ $s = $level == 1 ? “<xml>” : ‘’; foreach($arr as $tagname => $value) { if (is_numeric($tagname)) { $tagname = $value[‘TagName’]; unset($value[‘TagName’]); } if(!is_array($value)) { $s .= “<{$tagname}>”.(!is_numeric($value) ? ‘<![CDATA[’ : ‘’).$value.(!is_numeric($value) ? ‘]]>’ : ‘’)."</{$tagname}>”; }else { $s .= “<{$tagname}>” . $this->array2xml($value, $level + 1)."</{$tagname}>”; } } $s = preg_replace(”/([\x01-\x08\x0b-\x0c\x0e-\x1f])+/”, ’ ‘, $s); return $level == 1 ? $s."</xml>” : $s; } /* * 将xml转为array * @param string $xml xml字符串 * @return array 转换得到的数组 / protected function xml2array($xml) { //禁止引用外部xml实体 libxml_disable_entity_loader(true); $result= json_decode(json_encode(simplexml_load_string($xml, ‘SimpleXMLElement’, LIBXML_NOCDATA)), true); return $result; } /* * * 产生随机字符串,不长于32位 * @param int $length * @return 产生的随机字符串 / protected function getNonceStr($length = 32){ $chars = “abcdefghijklmnopqrstuvwxyz0123456789”; $str =""; for ( $i = 0; $i < $length; $i++ ) { $str .= substr($chars, mt_rand(0, strlen($chars)-1), 1); } return $str; } /* * 生成签名 * @return 签名 / protected function makeSign($data){ //获取微信支付秘钥 $key = $this->config[‘mch_secret’]; //去空 $data = array_filter($data); //签名步骤一:按字典序排序参数 ksort($data); $signParam = http_build_query($data); $signParam = urldecode($signParam); //签名步骤二:在string后加入KEY $signContent = $signParam."&key=".$key; //签名步骤三:MD5加密 $sign = md5($signContent); // 签名步骤四:所有字符转为大写 $result=strtoupper($sign); return $result; } /* * 微信支付发起请求 */ protected function request($url, $xmldata, $second=30, $aHeader=array()){ $ch = curl_init(); //超时时间 curl_setopt($ch,CURLOPT_TIMEOUT,$second); curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1); //这里设置代理,如果有的话 //curl_setopt($ch,CURLOPT_PROXY, ‘10.206.30.98’); //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); if( count($aHeader) >= 1 ){ curl_setopt($ch, CURLOPT_HTTPHEADER, $aHeader); } curl_setopt($ch,CURLOPT_POST, 1); curl_setopt($ch,CURLOPT_POSTFIELDS,$xmldata); $data = curl_exec($ch); if($data){ curl_close($ch); return $data; } else { $error = curl_errno($ch); echo “call faild, errorCode:$error\n”; curl_close($ch); return false; } }}踩坑点1、支付回调接口http://test.dev.com/wechat/pa… 一定要设置成get、post都能访问,我当初只设置了get请求可以访问,浪费了好多时间进行排查,而微信回调的数据基本都是以post形式进行调用的