摘要:背景事先准备工作申请一个小程序,并开通微信支付,详细见微信小程序支付业务说明仔细查阅微信支付官方文档,详细见微信支付开发者文档仔细阅读微信支付统一下单接口仔细阅读支付结果通知接口整理并在商户平台设置好相应的回掉地址,比如服务端编写两个接口微
背景 事先准备工作
申请一个小程序,并开通微信支付,详细见:微信小程序支付业务说明
仔细查阅微信支付官方文档,详细见: 微信支付开发者文档
仔细阅读 微信支付统一下单接口
仔细阅读 支付结果通知接口
整理并在商户平台设置好相应的回掉地址,比如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; }微信支付帮助类
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="踩坑点"; }else { $str=" "; } 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 ? " " : ""; foreach($arr as $tagname => $value) { if (is_numeric($tagname)) { $tagname = $value["TagName"]; unset($value["TagName"]); } if(!is_array($value)) { $s .= "<{$tagname}>".(!is_numeric($value) ? "" : "")."{$tagname}>"; }else { $s .= "<{$tagname}>" . $this->array2xml($value, $level + 1)."{$tagname}>"; } } $s = preg_replace("/([x01-x08x0b-x0cx0e-x1f])+/", " ", $s); return $level == 1 ? $s." " : $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 "; curl_close($ch); return false; } } }
1、支付回调接口http://test.dev.com/wechat/pa... 一定要设置成get、post都能访问,我当初只设置了get请求可以访问,浪费了好多时间进行排查,而微信回调的数据基本都是以post形式进行调用的
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/29871.html
摘要:目前支持哪些平台的搬家目前对外开放版本释放了微信小程序转支付宝小程序的功能,这也是我们在调研中发现需求最多的。从笔者的了解来看,微信小程序框架原理更接近于,而支付宝小程序更接近于。 原文地址: https://ant-move.github.io/we... 蚂蚁搬家工具(Antmove)是一个小程序开发辅助工具,致力于解决小程序跨平台开发的难题,借助于 Antmove,你只需要编写...
摘要:背景最近项目需要上线支付宝小程序,同时需要走用户的授权流程完成用户信息的存储,以前做过微信小程序的开发,本以为实现授权的过程是很简单的事情,但是再实现的过程中还是遇到了不少的坑,因此记录一下实现的过程学到的知识支付宝开放接口的调用模式以及实 背景 最近项目需要上线支付宝小程序,同时需要走用户的授权流程完成用户信息的存储,以前做过微信小程序的开发,本以为实现授权的过程是很简单的事情,但是...
摘要:按着我的步骤一步一步操作,你就可以成功的到这个微信支付技能包。原文链接手把手教你实现小程序微信支付由于自己本身就是开发的,所以只涉及到微信支付的开发。我将会一步一步的记录如何实现微信支付的。第一步先上微信支付开发文档境内普通商户里面下载与。 这是我自己研究了两天的微信支付整理得的开发笔记,然后在这里分享给大家,让大家快速上手微信支付。 按着我的步骤一步一步操作,你就可以成功的get到这...
摘要:这几天在做小程序的支付,没有用官方的,这里就纯用官方的文档搞一发。 这几天在做小程序的支付,没有用官方的SDK,这里就纯用官方的文档搞一发。 * 注作者使用的PHP,不过支付流程都是这样 开发前必读 主要流程 小程序前端发送求参请求 接受请求封装 统一下单 获取package 小程序接受 统一下单 获取的package值带入wx.requestPayment发起支付请求 准备...
阅读 1378·2021-11-24 09:38
阅读 2085·2021-09-22 15:17
阅读 2339·2021-09-04 16:41
阅读 3450·2019-08-30 15:56
阅读 3510·2019-08-29 17:19
阅读 1939·2019-08-28 18:09
阅读 1248·2019-08-26 13:35
阅读 1710·2019-08-23 17:52