文档章节

小程序服务端集成微信支付

秀杰
 秀杰
发布于 2017/01/03 11:06
字数 2217
阅读 11515
收藏 163

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

该demo源码已托管到码云:http://git.oschina.net/dotton/lendoo-wx,欢迎下载。

理论上集成微信支付的全部工作可以在小程序端完成,因为小程序js有访问网络的能力,但是为了安全,不暴露敏感key,也更好地跨平台(对于iOS与Android原生APP开发来说,哪天小程序的支付接口也纳入了开放平台了,这条也就成立了),而且可以使用官方提供的现成php demo更省力,于是在服务端完成签名与发起请求,小程序端只做一个wx.requestPayment(OBJECT)接口的对接。

整体集成过程与JSAPI、APP类似,先统一下单,然后拿返回的结果来请求支付。

一共三步:

1.小程序端通过wx.login的返回的code换取openid

2.服务端向微信统一下单

3.小程序端发起支付

事先准备好这几样东西:

APPID = 'wx426b3015555a46be';
MCHID = '1900009851';
KEY = '8934e7d15453e97507ef794cf7b0519d';
APPSECRET = '7813490da6f1265e4901ffb80afaa36f';

PHP SDK,下载链接见文尾

第1、4样是申请小程序时获得的,第2、3样是申请开通微信支付时获得的,注意第3、4样长得比较像,其实是2个东西,两者混淆将导致签名通不过

向微信端下单,得到prepay_id

1. 创建一个Controller,引并WxPay.Api.php类

<?php
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../third_party/wxpay/WxPay.Api.php';

class WXPay extends BaseController {
	function index() {
	}
}

之后可以通过index.php/wxpay来作访问请求

2. 修改配置文件WxPay.Config.php

改成自己申请得到相应key

3. 实现index方法

		function index() {
// 		初始化值对象
		$input = new WxPayUnifiedOrder();
// 		文档提及的参数规范:商家名称-销售商品类目
		$input->SetBody("灵动商城-手机");
// 		订单号应该是由小程序端传给服务端的,在用户下单时即生成,demo中取值是一个生成的时间戳
		$input->SetOut_trade_no('123123123');
// 		费用应该是由小程序端传给服务端的,在用户下单时告知服务端应付金额,demo中取值是1,即1分钱
		$input->SetTotal_fee("1");
		$input->SetNotify_url("http://paysdk.weixin.qq.com/example/notify.php");
		$input->SetTrade_type("JSAPI");
// 		由小程序端传给服务端
		$input->SetOpenid($this->input->post('openId'));
// 		向微信统一下单,并返回order,它是一个array数组
		$order = WxPayApi::unifiedOrder($input);
// 		json化返回给小程序端
		header("Content-Type: application/json");
		echo json_encode($order);
	}

**说明1:**文档上提到的nonce_str不是没提交,而是sdk帮我们填上的

出处在WxPay.Api.php第55行

$inputObj->SetNonce_str(self::getNonceStr());//随机字符串

**说明2:**sign也已经好心地给setSign了,出处在WxPay.Data.php第111行,MakeSign()中

	/**
	 * 生成签名
	 * @return 签名,本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值
	 */
	public function MakeSign()
	{
		//签名步骤一:按字典序排序参数
		ksort($this->values);
		$string = $this->ToUrlParams();
		//签名步骤二:在string后加入KEY
		$string = $string . "&key=".WxPayConfig::KEY;
		//签名步骤三:MD5加密
		$string = md5($string);
		//签名步骤四:所有字符转为大写
		$result = strtoupper($string);
		return $result;
	}

4. 小程序内调用登录接口,获取openid

向微信登录请求,拿到code,再将code提交换取openId

wx.login({
	      success: function(res) {
	        if (res.code) {
	          //发起网络请求
	          wx.request({
	            url: 'https://api.weixin.qq.com/sns/jscode2session?appid=wx9114b997bd86f***&secret=d27551c7803cf16015e536b192******&js_code='+res.code+'&grant_type=authorization_code',
	            data: {
	              code: res.code
	            },
	            success: function (response) {
	            	console.log(response);
	            }
	          })
	        } else {
	          console.log('获取用户登录态失败!' + res.errMsg)
	        }
	      }
	    });

从控制台看到已经成功拿到openid,剩下的事情就是将它传到服务端就好了,服务端那边$this->input->post('openId')等着收呢。

输入图片说明

5. 小程序端向https://lendoo.leanapp.cn/index.php/WXPay发起请求,作统一下单

	            	//统一下单接口对接
	            	wx.request({
	            		url: 'https://lendoo.leanapp.cn/index.php/WXPay',
	            		data: {
	            			openId: openId
	            		},
	            		success: function (response) {
	            			console.log(response);

	            		},
                                header: {
				        'content-type': 'application/x-www-form-urlencoded'
				},
	            	});

得到如下结果

{
  "appid": "wx9114b997bd86f8ed",
  "mch_id": "1414142302",
  "nonce_str": "eEICgYFuGqxFRK6f",
  "prepay_id": "wx201701022235141fc713b8f80137935406",
  "result_code": "SUCCESS",
  "return_code": "SUCCESS",
  "return_msg": "OK",
  "sign": "63E60C8CD90394FB50E612D085F5362C",
  "trade_type": "JSAPI"
}

前提是https://lendoo.leanapp.cn已经在白名单:

输入图片说明

6. 小程序端调起支付API

// 发起支付
var appId = response.data.appid;
var timeStamp = (Date.parse(new Date()) / 1000).toString();
var pkg = 'prepay_id=' + response.data.prepay_id;
var nonceStr = response.data.nonce_str;
var paySign = md5.hex_md5('appId='+appId+'&nonceStr='+nonceStr+'&package='+pkg+'&signType=MD5&timeStamp='+timeStamp+"&key=d27551c7803cf16***e536b192d5d03b").toUpperCase();
console.log(paySign);
console.log(appId);
wx.requestPayment({
	'timeStamp': timeStamp,
	'nonceStr': nonceStr,
	'package': pkg,
	'signType': 'MD5',
	'paySign': paySign,
	'success':function(res){
		console.log('success');
		console.log(res);
	}
});

模拟器测试,将弹出一个二维码供扫描

输入图片说明

结果报了一个错误:

输入图片说明

errMsg:"requestPayment:fail"
err_code:2
err_desc:"支付验证签名失败"

key需要加入到签名中!!! 'appId='+appId+'&nonceStr='+nonceStr+'&package='+pkg+'&signType=MD5&timeStamp='+timeStamp+"&key=d27551c7803cf16***e536b192d5d03b"这才是完整的。

可是文档里明明没提到key啊

输入图片说明

7. 支付成功截图

输入图片说明

输入图片说明

吐槽完文档再吐槽下命名规则,GetSpbill_create_ip()、IsSpbill_create_ipSet()都是些什么鬼一会儿下划线分隔一会儿驼峰分隔,成员方法首字母还大写,unifiedOrder()这种正经写法也不忘来比划两下,看来网上说大公司的sdk都是实习生撰写是真事,可code reviewer又在哪里?

【2017-1-5】

番外篇 - 服务端生成与二次签名与参数

之前的作法可视为服务端与小程序双重请求混合使用的一个范例。

接下来将演示纯服务端的方式,小程序只关注wx.requestPayment对接。

二次签名也通过服务端,让小程序端调用更加简洁,前端不再关心生成timeStamp,md5,paySign,全部将服务端返回得到。

这种做法应该是更为主流的作法。

        public function getJsApiParameters($UnifiedOrderResult)
    {
        if(!array_key_exists("appid", $UnifiedOrderResult)
        || !array_key_exists("prepay_id", $UnifiedOrderResult)
        || $UnifiedOrderResult['prepay_id'] == "")
        {
            throw new WxPayException("参数错误");
        }
        $jsapi = new WxPayJsApiPay();
        $jsapi->SetAppid($UnifiedOrderResult["appid"]);
        $timeStamp = time();
        $jsapi->SetTimeStamp("$timeStamp");
        $jsapi->SetNonceStr(WxPayApi::getNonceStr());
        $jsapi->SetPackage("prepay_id=" . $UnifiedOrderResult['prepay_id']);
        $jsapi->SetSignType("MD5");
        $jsapi->SetPaySign($jsapi->MakeSign());
        $parameters = json_encode($jsapi->GetValues());
        return $parameters;
    }

调用getJsApiParameters

//         json化返回给小程序端
header("Content-Type: application/json");
echo $this->getJsApiParameters($order);

小程序端发起支付

//统一下单接口对接
wx.request({
	url: 'https://lendoo.leanapp.cn/index.php/WXPay',
	data: {
		openId: openId
	},
	method: 'POST',
	header: {
	    'content-type': 'application/x-www-form-urlencoded'
	},
	success: function (response) {
    	// 发起支付
    	wx.requestPayment({
			'timeStamp': response.data.timeStamp,
			'nonceStr': response.data.nonceStr,
			'package': response.data.package,
			'signType': 'MD5',
			'paySign': response.data.paySign,
			'success':function(res){
				console.log('success');
				console.log(res);
			}
		});
	}
});

省去了timeStamp、paySign的生成,连带md5.js也省掉了,小程序端调用起来就清爽多了。

中间出现了一个小插曲,package被我画蛇添足地写成了"prepay_id_"+response.data.package

报了缺少参数total_fee的错误,既不是报参数错误,也不是报prepay_id错误,相当无厘头。

输入图片说明

【2017-1-14】再次简化小程序端,将jscode2session请求移到服务端完成

wx.login({
	success: function(res) {
		if (res.code) {
        	//统一下单接口对接
        	wx.request({
        		url: 'https://lendoo.leanapp.cn/index.php/WXPay',
        		data: {
        			code: res.code
        		},
        		method: 'POST',
        		header: {
        			'content-type': 'application/x-www-form-urlencoded'
        		},
        		success: function (response) {
	            	// 发起支付
					wx.requestPayment({
						'timeStamp': response.data.timeStamp,
						'nonceStr': response.data.nonceStr,
						'package': response.data.package,
						'signType': 'MD5',
						'paySign': response.data.paySign,
						'success':function(res){
							wx.showToast({
								title: '支付成功'
							});
							// console.log(res);
						}
					});
				}
			});
        } else {
        	console.log('获取用户登录态失败!' + res.errMsg)
        }
    }
});

如上所示,https://lendoo.leanapp.cn/index.php/WXPay已经完成了jscode2session的操作,即拿到了openid,这一切对于小程序端都是透明的,把工作交给服务端来完成。

	private function getSession() {
		$code = $this->input->post('code');
		$url = 'https://api.weixin.qq.com/sns/jscode2session?appid='.WxPayConfig::APPID.'&secret='.WxPayConfig::APPSECRET.'&js_code='.$code.'&grant_type=authorization_code';
		$response = json_decode(file_get_contents($url));
		return $response;
	}

原index()方法也相应地,从$input->SetOpenid($this->input->post('openid'));改为$input->SetOpenid($this->getSession()->openid);

如此一来,小程序端更简洁清楚,减少了一次网络请求,也更好的保护了app_id与app_secret不在前端暴露。

附上最终的服务端代码:

<?php
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../third_party/wxpay/WxPay.Api.php';

class WXPay extends BaseController {
	function index() {
		// 		初始化值对象
		$input = new WxPayUnifiedOrder();
		// 		文档提及的参数规范:商家名称-销售商品类目
		$input->SetBody("灵动商城-手机");
		// 		订单号应该是由小程序端传给服务端的,在用户下单时即生成,demo中取值是一个生成的时间戳
		$input->SetOut_trade_no(time().'');
		// 		费用应该是由小程序端传给服务端的,在用户下单时告知服务端应付金额,demo中取值是1,即1分钱
		$input->SetTotal_fee("1");
		$input->SetNotify_url("http://paysdk.weixin.qq.com/example/notify.php");
		$input->SetTrade_type("JSAPI");
		// 		由小程序端传给服务端
		$input->SetOpenid($this->getSession()->openid);
		// 		向微信统一下单,并返回order,它是一个array数组
		$order = WxPayApi::unifiedOrder($input);
		// 		json化返回给小程序端
		header("Content-Type: application/json");
		echo $this->getJsApiParameters($order);
	}

	private function getJsApiParameters($UnifiedOrderResult)
	{
		if(!array_key_exists("appid", $UnifiedOrderResult)
		|| !array_key_exists("prepay_id", $UnifiedOrderResult)
		|| $UnifiedOrderResult['prepay_id'] == "")
		{
			throw new WxPayException("参数错误");
		}
		$jsapi = new WxPayJsApiPay();
		$jsapi->SetAppid($UnifiedOrderResult["appid"]);
		$timeStamp = time();
		$jsapi->SetTimeStamp("$timeStamp");
		$jsapi->SetNonceStr(WxPayApi::getNonceStr());
		$jsapi->SetPackage("prepay_id=" . $UnifiedOrderResult['prepay_id']);
		$jsapi->SetSignType("MD5");
		$jsapi->SetPaySign($jsapi->MakeSign());
		$parameters = json_encode($jsapi->GetValues());
		return $parameters;
	}
	
	private function getSession() {
		$code = $this->input->post('code');
		$url = 'https://api.weixin.qq.com/sns/jscode2session?appid='.WxPayConfig::APPID.'&secret='.WxPayConfig::APPSECRET.'&js_code='.$code.'&grant_type=authorization_code';
		$response = json_decode(file_get_contents($url));
		return $response;
	}
}

输入图片说明

【正文完】

本文涉及代码存于项目根目录/app.js与/pages/order/payment中

小程序端文档出处:https://mp.weixin.qq.com/debug/wxadoc/dev/api/api-pay.html#wxrequestpaymentobject

微信支付服务端侧文档出处:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1

类比文档出处:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1

开发步骤:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_3&index=1

sdk下载:https://pay.weixin.qq.com/wiki/doc/api/download/WxpayAPI_php_v3.zip

对移动开发有兴趣的朋友可以关注我的公众号【huangxiujie85】与我交流讨论,给我留言或文章评论。

公众号huangxiujie85

© 著作权归作者所有

秀杰
粉丝 153
博文 94
码字总数 50956
作品 0
瑞安
iOS工程师
私信 提问
加载中

评论(51)

秀杰
秀杰 博主

引用来自“霍霍php”的评论

引用来自“霍霍php”的评论

老师,您好!我是一个新手,有的地方没有看懂,想请教一下。
<?php
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../third_party/wxpay/WxPay.Api.php';

class WXPay extends BaseController {
  function index() {
  }
}
为什么直接写function index(){},而不是public function index(){}

引用来自“秀杰”的评论

默认就是 public,写不写都是一样。除非 private
老师,你的路径写的是index.php/wxpay,访问的是controllers/WXPay.php/index()这个方法吗?还有,下载您的后台代码,代码是番外篇的,还是番外篇上面那一种方法的?

回复@霍霍php : 番外篇就是最后那种,从服务端生成预下单签名信息等,简洁明了。
霍霍php
霍霍php

引用来自“霍霍php”的评论

老师,您好!我是一个新手,有的地方没有看懂,想请教一下。
<?php
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../third_party/wxpay/WxPay.Api.php';

class WXPay extends BaseController {
  function index() {
  }
}
为什么直接写function index(){},而不是public function index(){}

引用来自“秀杰”的评论

默认就是 public,写不写都是一样。除非 private
老师,你的路径写的是index.php/wxpay,访问的是controllers/WXPay.php/index()这个方法吗?还有,下载您的后台代码,代码是番外篇的,还是番外篇上面那一种方法的?
秀杰
秀杰 博主

引用来自“霍霍php”的评论

老师,您好!我是一个新手,有的地方没有看懂,想请教一下。
<?php
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../third_party/wxpay/WxPay.Api.php';

class WXPay extends BaseController {
  function index() {
  }
}
为什么直接写function index(){},而不是public function index(){}
默认就是 public,写不写都是一样。除非 private
霍霍php
霍霍php
老师,您好!我是一个新手,有的地方没有看懂,想请教一下。
<?php
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../third_party/wxpay/WxPay.Api.php';

class WXPay extends BaseController {
  function index() {
  }
}
为什么直接写function index(){},而不是public function index(){}
秀杰
秀杰 博主

引用来自“思K”的评论

引用来自“秀杰”的评论

引用来自“思K”的评论

看了很多资料,看到这篇终于有点开窍了👍
之前没有搞过jsapi 原来也是用同一个sdk

回复@思K : 可以这么说
请问假如用户支付成功了 商家是怎么知道 和后台改变的订单状态啊

回复@思K : 人肉看后台,哈哈
思K
思K

引用来自“秀杰”的评论

引用来自“思K”的评论

看了很多资料,看到这篇终于有点开窍了👍
之前没有搞过jsapi 原来也是用同一个sdk

回复@思K : 可以这么说
请问假如用户支付成功了 商家是怎么知道 和后台改变的订单状态啊
秀杰
秀杰 博主

引用来自“思K”的评论

看了很多资料,看到这篇终于有点开窍了👍
之前没有搞过jsapi 原来也是用同一个sdk

回复@思K : 可以这么说
思K
思K
看了很多资料,看到这篇终于有点开窍了👍
之前没有搞过jsapi 原来也是用同一个sdk
秀杰
秀杰 博主

引用来自“绿果源”的评论

微信支付我搞不来,在哪里改微信支付的id

回复@绿果源 : WxpayAPI_php_v3.zip在放在服务端的,我服务端也已经托管了,https://git.oschina.net/dotton/lendoo-web
秀杰
秀杰 博主

引用来自“绿果源”的评论

微信支付我搞不来,在哪里改微信支付的id

回复@绿果源 : WxPay.Config.php文件的第25-28行
移动支付系统搭建代理,移动扫码支付系统贴牌开发有哪些优势

移动扫码支付成为了主流的支付模式,那么移动扫码支付存在哪些优势?我们有可以从中获取哪些收益点? 移动扫码支付优势——减少商家运营成本,提高运营效率——增加消费群体的粘性,绑定老顾...

qq_38621102
2018/05/11
0
0
PHP 支付类库 PaySDK v1.0.9 新增支付宝 APP 支付

PaySDK 是 PHP 集成支付 SDK ,集成了支付宝、微信支付的支付接口和其它相关接口的操作。可以轻松嵌入支持 PHP >= 5.4 的任何系统中。 从 v1.0.7 - v1.0.9 的更新内容如下: 支持支付宝APP支...

宇润
2018/04/04
1K
4
iOS微信支付接入以及工具类封装

在刚刚结束的一个项目中用到了微信支付,从接入微信支付到工具类的封装,在本文中做个积累,方便日后使用。1.开始接入微信支付的准备工作 首先你需要去微信开放平台注册账号,在这里要吐槽一下...

储小白
2018/07/06
0
0
iOS集成微信支付--Swift

微信支付在微信红包的推动发展势头越来越猛,甚至有超过支付宝的趋势,那么在App集成微信支付也是比不可少了。我最近在一个项目中集成微信支付遇到了不少问题,Google了不少资料才搞定,不得...

上官尘
2016/02/26
998
0
小柒2012/spring-boot-pay

spring-boot-pay 支付服务:支付宝,微信,银联详细 代码案例 (除银联支付可以测试以外,支付宝和微信支付测试均需要企业认证,个人无法完成测试),项目启动前请仔细阅读 注意事项 。 API接口...

小柒2012
2017/08/02
0
0

没有更多内容

加载失败,请刷新页面

加载更多

聊聊rocketmq producer的batch

序 本文主要研究一下rocketmq producer的batch batch rocketmq-client-4.6.0-sources.jar!/org/apache/rocketmq/client/producer/DefaultMQProducer.java public class DefaultMQProducer ex......

go4it
昨天
5
0
Delphi中的延时

开发过程中经常会需要使用到延时功能,Delphi中有不少实现延时的方法,网上已有不少文章做过说明和分析,但本着实践出真知的态度,还是亲自动手研究一番心里比较踏实。 常用的延时方法 Slee...

天朝八阿哥
昨天
5
0
001-Consul

Consul安装(单节点) mkdir -p /data/consulcd /data/consulwget https://releases.hashicorp.com/consul/1.6.2/consul_1.6.2_linux_amd64.zipunzip consul_1.6.2_linux_amd64.zip复制c......

伟大源于勇敢的开始
昨天
5
0
nginx + frp 搭建内网穿透

上一个项目是开发微信公众号,由于微信的各种烦人操作,只能到处找内网映射工具 ngrok也用过,花生壳也用过 都不怎么稳定,无意间听说了frp,本着一颗折腾的心搭建了一下,结果发现很不错,就...

lineasy
昨天
8
0
构建CRD工程 - 程序员学点xx 43 k8s

Kubernetes -3- <!--more--> <center>这是yann的第98篇分享</center> [TOC] 本日状态: 帮同事排了一天bug。 Kubernetes -3- <!--more--> 这是yann的第98篇分享 第 1 部分 承前 昨天用视屏的方......

tmp4
昨天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部