根据微信支付的官方文档,小程序支付需要绑定商户号,并且小程序和商户号的认证主体必须一致。
目前我们的商业逻辑是小程序平台主体和支付的商户主体不一致,
那么就需要从我们的小程序跳转到支付主体小程序完成支付后,再返回我们的小程序。
曾经尝试过在小程序中通过webview的方式嵌套H5网页,使用公众号支付方式,后来发现小程序并未开放这个JSAPI。
接下来详细介绍一下如何实现小程序之间的跳转支付。
首先我已经有了2个小程序:
- 平台小程序A,认证主体是A公司
- 支付小程序B,认证主体是B公司
接入微信支付
支付小程序B要接入微信支付,必须要先注册并认证微信公众号,然后申请服务商商户,
具体怎么申请参考微信支付服务商平台
然后登录小程序后台,点击左侧导航栏的微信支付,在页面中进行开通。
点击开通按钮后,有2种方式可以获取微信支付能力,新申请微信支付商户号或绑定一个已有的微信支付商户号,
请根据你的业务需要和具体情况选择,只能二选一。
开通指引:http://kf.qq.com/faq/140225MveaUz161230yqiIby.html
小程序支付开发注意点:
- appid 必须为最后拉起收银台的小程序appid;
- mch_id 为和appid成对绑定的支付商户号,收款资金会进入该商户号;
- trade_type 请填写JSAPI;
- openid 为appid对应的用户标识,即使用wx.login接口获得的openid
小程序跳转
参考官方文档打开小程序
wx.navigateToMiniProgram(OBJECT)
,打开同一公众号下关联的另一个小程序。(注:必须是同一公众号下,而非同个open账号下)
所以第一步要将两个小程序关联到同一个公众号上面去,这里一般都会关联到平台的公众号上去。
关联操作需要从公众号发起,请登录公众平台上公众号的管理后台,在”设置-公众号设置-相关小程序”中进行关联。
然后小程序的管理员会受到一条微信消息,点击进去同意即可。之后再进入小程序后台,就能看到小程序所关联的公众号了:
小程序A跳转到B的示例代码:
1 2 3 4 5 6 7 8 9 10 11
| wx.navigateToMiniProgram({ appId: 'APPID', path: 'pages/index/index?id=123', extraData: { foo: 'bar' }, envVersion: 'develop', success(res) { } })
|
同时还可以给小程序B传递参数,通过extraData
这个参数,小程序B接受参数的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| App({ onLaunch: function(options) { wx.setStorageSync('fromAppId', options.referrerInfo.appId) wx.setStorageSync('foo', options.referrerInfo.extraData.foo) }, onShow: function(options) { }, onHide: function() { }, onError: function(msg) { console.log(msg) }, globalData: 'I am global data' })
|
小程序B处理完成后返回小程序A的方式:
1 2 3 4 5 6 7 8 9
| console.log('支付成功啦啦啦啦。。。。。') wx.navigateBackMiniProgram({ extraData: { payResult: 'good' }, success(res) { } })
|
然后小程序A在App.js
中接受到返回值:
1 2 3 4
| onShow: function (options) { wx.setStorageSync('payResult', options.referrerInfo.extraData.payResult) } ,
|
然后在相应的页面就能处理结果值了,这里还是用onShow函数:
1 2 3 4 5 6
| onShow: function (options) { this.setData({ 'lalala': wx.getStorageSync('payResult') }) } ,
|
小程序支付
小程序B需要实现微信支付功能,还需要有一个后端系统,我这里通过一个SpringBoot工程搭建后端系统,
另外引入一个微信支付的依赖:
1 2 3 4 5
| <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>${weixin-java-pay.version}</version> </dependency>
|
主要的逻辑是这样:
- 小程序向服务端发送商品详情、金额、openid
- 服务端向微信统一下单
- 服务器收到返回信息二次签名发回给小程序
- 小程序发起支付
- 服务端收到回调
这里需要注意的是,小程序调用后台接口的地址需要在小程序后台配置好服务器域名:
服务器域名只能是https开头,所以需要先去给你的网站申请一个SSL证书,配置一下nginx来代理转发即可,
比如我的域名为https://aggrepay.enzhico.cn/
,这里我申请的是lets encrypted证书:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| server { listen 443 ssl; server_name aggrepay.enzhico.cn;
location / { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header 'Access-Control-Allow-Origin' '*'; proxy_set_header 'Access-Control-Allow-Credentials' 'true'; proxy_set_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,X-Requested-With'; proxy_set_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS'; proxy_pass http://119.29.12.177:8086/; }
ssl_certificate /etc/letsencrypt/live/aggrepay.enzhico.cn/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/aggrepay.enzhico.cn/privkey.pem;
ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets on;
ssl_dhparam /etc/ssl/private/dhparam.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK'; ssl_prefer_server_ciphers on; }
|
设置完服务器后,就将后台应用部署上去。
回到小程序B中,先要获取到用户openid,小程序是通过登录操作获取到用户的openid的。
参考官方的小程序登录API
很重要的一张登录时序图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| onLoad: function () { var that = this wx.login({ success: function (res) { console.log("code = " + res.code) that.getOpenId(res.code) } }); } , getOpenId: function (code) { var that = this; wx.request({ url: "https://aggrepay.enzhico.cn/pay/openid?code=" + code, data: {}, method: 'GET', success: function (res) { console.log("res.openid = " + res.data.openid) that.generateOrder(res.data.openid) }, fail: function () { }, complete: function () { } }) } ,
|
对应后台代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@RequestMapping(value = "/openid", method = RequestMethod.GET) @ResponseBody public String openid(@RequestParam(value = "code") String code) throws Exception { logger.info("/openid start...."); CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet("https://api.weixin.qq.com/sns/jscode2session?" + "appid=APPID&secret=SECRET&js_code=" + code + "&grant_type=authorization_code"); try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) { logger.info(response1.getStatusLine().toString()); String result = EntityUtils.toString(response1.getEntity(), StandardCharsets.UTF_8); logger.info("result=" + result); return result; } }
|
获取到openid后就可以调用统一下单接口了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| generateOrder: function (openid) { var that = this wx.request({ url: 'https://aggrepay.enzhico.cn/pay/unifiedOrder', method: 'POST', data: { outTradeNo: Math.random().toString(36).substring(7), totalFee: '7', body: '支付测试', attach: '云塔山香烟', spbillCreateIp: '123.267.12.2', notifyURL: 'https://aggrepay.enzhico.cn/pay/parseOrderNotifyResult', tradeType: 'JSAPI', openid: openid }, success: function (res) { var timeStamp = res.data.timeStamp; var packages = 'prepay_id=' + res.data.prepayId; var paySign = res.data.sign; var nonceStr = res.data.nonceStr; var param = { "timeStamp": timeStamp, "package": packages, "paySign": paySign, "signType": "MD5", "nonceStr": nonceStr }; that.pay(param) }, }) } ,
|
对应后台代码,注意二次签名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
@PostMapping("/unifiedOrder") public WxPayMpOrderResultVO unifiedOrder2(@RequestBody WxPayUnifiedOrderRequest request) throws WxPayException { WxPayUnifiedOrderResult result = this.wxService.unifiedOrder(request); logger.info(result.getReturnMsg()); String signType = WxPayConstants.SignType.MD5; WxPayMpOrderResultVO payResult = new WxPayMpOrderResultVO(); payResult.setAppId(result.getAppid()); payResult.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000)); payResult.setNonceStr(result.getNonceStr()); payResult.setPackageValue("prepay_id=" + result.getPrepayId()); payResult.setSignType(signType); payResult.setPrepayId(result.getPrepayId()); payResult.setSign(createSign(payResult, this.wxService.getConfig().getMchKey())); return payResult; }
private String createSign(WxPayMpOrderResultVO p, String mchKey) { String tosstr = "appId="+p.getAppId()+"&nonceStr="+p.getNonceStr()+"&package="+p.getPackageValue() +"&signType=MD5&timeStamp="+p.getTimeStamp()+"&key=" + mchKey; logger.info(tosstr); return DigestUtils.md5Hex(tosstr).toUpperCase(); }
|
拿到预支付订单后,小程序B就能调用支付了,支付成功后返回到小程序A中,并将支付结果通过extraData
参数回传过去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| pay: function (param) { console.log("支付") console.log(param) wx.requestPayment({ 'timeStamp': param.timeStamp, 'nonceStr': param.nonceStr, 'package': param.package, 'signType': param.signType, 'paySign': param.paySign, success: function (res) { console.log('支付成功啦啦啦啦。。。。。') wx.navigateBackMiniProgram({ extraData: { payResult: 'good' }, success(res) { } }) }, fail: function (res) { }, complete: function () { } }) }
|
小程序的跳转需要上线版本才能看到效果,所以需要把代码上传后,提交上线审核。
登录小程序后台,点击”开发管理”,在下面的开发版本中点击提交审核,一般半天左右审核完成。