{ 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的形式进行请求php
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),字段以下redis
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;
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('取消微信支付') } } }) },
一、入口方法: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; }
二、微信核心支付方法:doWxaPayjson
/** * 经过小程序支付的逻辑 * @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; }
三、建立订单方法: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; }
四、订单查重方法: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; }
五、生成订单方法:api
/** * 生成订单,辅助方法 * @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; }
六、生成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; }
<?php namespace 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; } } }
一、支付回调接口http://test.dev.com/wechat/pa... 必定要设置成get、post都能访问,我当初只设置了get请求能够访问,浪费了好多时间进行排查,而微信回调的数据基本都是以post形式进行调用的