引言
相信大家都会对微信支付非常熟悉,什么样的开发场景能少得了支付的环节呢?
Let's do it~
开发接入官方文档阅读
不知几何时起,有人就跟我提过,微信小程序的开发离不开微信官方文档的说明,需要反复看个多遍,其义自见,请允许我多说了这些废话。
参考阅读微信支付官方参考文档
因为小程序调起支付页面的协议是HTTPS(程序访问商户服务都是通过HTTPS)
开发部署的时候需要安装HTTPS服务器(此处就不延伸扩展安装https服务了)
现提供另外一种方式,阿里云申请免费SSL证书并在Nginx上进行安装
业务说明
业务流程
商户系统和微信支付系统主要交互:
1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】
2、商户server调用支付统一下单,api参见公共api【统一下单API】
3、商户server调用再次签名,api参见公共api【再次签名】
4、商户server接收支付通知,api参见公共api【支付结果通知API】
5、商户server查询支付结果,api参见公共api【查询订单API】
微信支付 Java后端
前面赘述了那么多官方文档的开发简介,下面开始真正的实战!
微信小程序支付的前置准备:
- 1.微信商户平台账号
- 2.微信小程序账号
- 3.微信小程序开通支付接口(个人暂时不支持开通支付)
前台需要的操作:
- 1.登录获取code,传给开发者后台
- 6.获取后台传过来的值调用wx.requestPayment方法
后台需要的操作:
- 2.通过前台传过来的code来获取用户的openId
- 3.生成sign
- 4.获取perpay_id
- 5.再生成一次前台需要的paySign
一、小程序登录API
具体操作:
- 小程序调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
#示例代码
//app.js
App({
onLaunch: function() {
wx.login({
success: function(res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://test.com/onLogin',
data: {
code: res.code
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
});
}
})
- 将临时登录凭证code回传到开发者服务器
wx.request({
url: 'https://URL',//后台URL
data: {},//登录获取的code
method: 'GET', // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
// header: {}, // 设置请求的 header//后台规定的请求头
success: function(res){
// success
},
fail: function(res) {
// fail
},
complete: function(res) {
// complete
}
})
- 登录凭证校验
此波操作是在开发者服务器进行的。
临时登录凭证校验接口是一个 HTTPS 接口,开发者服务器使用 临时登录凭证code 获取 session_key 和 openid 等。
开发者服务器通过code获取openid只需要访问这个链接:
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
请求参数
参数 | 必填 | 说明 |
---|---|---|
appid | 是 | 小程序唯一标识 |
secret | 是 | 小程序的 app secret |
js_code | 是 | 登录时获取的 code |
grant_type | 是 | 填写为 authorization_code |
在不满足UnionID下发条件的情况下,返回参数
参数 | 说明 |
---|---|
openid | 用户唯一标识 |
session_key | 会话密钥 |
在满足UnionID下发条件的情况下,返回参数
参数 | 说明 |
---|---|
openid | 用户唯一标识 |
session_key | 会话密钥 |
unionid | 用户在开放平台的唯一标识符 |
注意:
会话密钥session_key 是对用户数据进行加密签名的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
UnionID 只在满足一定条件的情况下返回。具体参看UnionID机制说明
临时登录凭证code只能使用一次
返回说明
//正常返回的JSON数据包
{
"openid": "OPENID",
"session_key": "SESSIONKEY",
}
//满足UnionID返回条件时,返回的JSON数据包
{
"openid": "OPENID",
"session_key": "SESSIONKEY",
"unionid": "UNIONID"
}
//错误时返回JSON数据包(示例为Code无效)
{
"errcode": 40029,
"errmsg": "invalid code"
}
统一下单API
商户在小程序中先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易后调起支付。
- 接口链接
URL地址:
https://api.mch.weixin.qq.com/pay/unifiedorder
这个链接必须是xml格式的,前台是没法调用的。
请求参数
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
小程序ID | appid | 是 | String(32) | wxd678efh567hg6787 | 微信分配的小程序ID |
商户号 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,长度要求在32位以内。推荐随机数生成算法 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 通过签名算法计算得出的签名值,详见签名生成算法 |
签名类型 | sign_type | 否 | String(32) | MD5 | 签名类型,默认为MD5,支持HMAC-SHA256和MD5。 |
商品描述 | body | 是 | String(128) | 腾讯充值中心-QQ会员充值 | 商品简单描述,该字段请按照规范传递,具体请见参数规定 |
商品详情 | detail | 否 | String(6000) | 商品详细描述,对于使用单品优惠的商户,改字段必须按照规范上传,详见“单品优惠参数说明” | |
附加数据 | attach | 否 | String(127) | 深圳分店 | 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 |
商户订单号 | out_trade_no | 是 | String(32) | 20150806125346 | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一。详见商户订单号 |
标价币种 | fee_type | 否 | String(16) | CNY | 符合ISO 4217标准的三位字母代码,默认人民币:CNY,详细列表请参见货币类型 |
标价金额 | total_fee | 是 | Int 88 | 订单总金额,单位为分,详见支付金额 | |
终端IP | spbill_create_ip | 是 | String(16) | 123.12.12.123 | APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 |
交易起始时间 | time_start | 否 | String(14) | 20091225091010 | 订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。其他详见时间规则 |
交易结束时间 | time_expire | 否 | String(14) | 20091227091010 | 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id。其他详见时间规则 建议:最短失效时间间隔大于1分钟 |
订单优惠标记 | goods_tag | 否 | String(32) | WXG 订单优惠标记,使用代金券或立减优惠功能时需要的参数,说明详见代金券或立减优惠 | |
通知地址 | notify_url | 是 | String(256) | http://www.weixin.qq.com/wxpay/pay.php | 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 |
交易类型 | trade_type | 是 | String(16) | JSAPI | 小程序取值如下:JSAPI,详细说明见参数规定 |
商品ID | product_id | 否 | String(32) | 12235413214070356458058 | trade_type=NATIVE时(即扫码支付),此参数必传。此参数为二维码中包含的商品ID,商户自行定义。 |
指定支付方式 | limit_pay | 否 | String(32) | no_credit | 上传此参数no_credit--可限制用户不能使用信用卡支付 |
用户标识 | openid | 否 | String(128) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o | trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。openid如何获取,可参考【获取openid】。 |
举例如下:
<xml>
<appid>wx2421b1c4370ec43b</appid>
<attach>支付测试</attach>
<body>JSAPI支付测试</body>
<mch_id>10000100</mch_id>
<detail><![CDATA[{ "goods_detail":[ { "goods_id":"iphone6s_16G", "wxpay_goods_id":"1001", "goods_name":"iPhone6s 16G", "quantity":1, "price":528800, "goods_category":"123456", "body":"苹果手机" }, { "goods_id":"iphone6s_32G", "wxpay_goods_id":"1002", "goods_name":"iPhone6s 32G", "quantity":1, "price":608800, "goods_category":"123789", "body":"苹果手机" } ] }]]></detail>
<nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str>
<notify_url>http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php</notify_url>
<openid>oUpF8uMuAJO_M2pxb1Q9zNjWeS6o</openid>
<out_trade_no>1415659990</out_trade_no>
<spbill_create_ip>14.23.150.211</spbill_create_ip>
<total_fee>1</total_fee>
<trade_type>JSAPI</trade_type>
<sign>0CB01533B8C1EF103065174F50BCA001</sign>
</xml>
注:参数值用XML转义即可,CDATA标签用于说明数据不被XML解析器解析。
总结一下请求参数必填项
字段名 | 变量名 | 描述 |
---|---|---|
小程序ID | appid | 微信分配的小程序ID |
商户号 | mch_id | 微信支付分配的商户号 |
随机字符串 | nonce_str | 随机字符串,长度要求在32位以内。推荐随机数生成算法 |
签名 | sign | 通过签名算法计算得出的签名值,详见签名生成算法 |
商品描述 | body | 商品简单描述,该字段请按照规范传递,具体请见参数规定 |
商户订单号 | out_trade_no | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-*且在同一个商户号下唯一。详见商户订单号 |
标价金额 | total_fee | 订单总金额,单位为分,详见支付金额 |
终端IP | spbill_create_ip | APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 |
通知地址 | notify_url | 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 |
交易类型 | trade_type | 小程序取值如下:JSAPI,详细说明见参数规定 |
用户标识 | openid | trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。openid如何获取,可参考【获取openid】。 |
以上提及的参数格式参考微信支付参数规定
返回结果
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 |
返回信息 | return_msg | 否 | String(128) 签名失败 | 返回信息,如非空,为错误原因 | 签名失败 参数格式校验错误 |
以下字段在return_code为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
小程序ID | appid | 是 | String(32) | wx8888888888888888 | 调用接口提交的小程序ID |
商户号 | mch_id | 是 | String(32) | 1900000109 | 调用接口提交的商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 自定义参数,可以为请求支付的终端设备号等 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 微信返回的随机字符串 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 微信返回的签名值,详见签名算法 |
业务结果 | result_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL |
错误代码 | err_code | 否 | String(32) | SYSTEMERROR 详细参见下文错误列表 | |
错误代码描述 | err_code_des | 否 | String(128) | 系统错误 错误信息描述 |
以下字段在return_code 和result_code都为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
交易类型 | trade_type | 是 | String(16) | JSAPI | 交易类型,取值为:JSAPI,NATIVE,APP等,说明详见参数规定 |
预支付交易会话标识 | prepay_id | 是 | String(64) | wx201410272009395522657a690389285100 | 微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时 |
二维码链接 | code_url | 否 | String(64) | URl:weixin://wxpay/s/An4baqw | trade_type为NATIVE时有返回,用于生成二维码,展示给用户进行扫码支付 |
举例如下:
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<mch_id><![CDATA[10000100]]></mch_id>
<nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
<openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
<sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
<result_code><![CDATA[SUCCESS]]></result_code>
<prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
<trade_type><![CDATA[JSAPI]]></trade_type>
</xml>
此大项步骤用于生成sign和得到prepay_id(此处后台用JAVA由于后台代码太多,我就放在最后面,有需要的可以自己进行查看)
其中获取prepay_id生成的sign需要的参数是我遇到的一个坑,请一定要注意你往微信传的值都是sign需要的value,其中openId在参数必填里面写的是“否”,但是微信小程序支付用到的trade_type是JSAPI,所以微信小程序的openId也是必须的参数。
当这些都准备好了之后你就可以获取perpay_id。
再次签名即为小程序调起支付API
小程序调起支付数据签名字段列表:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
小程序ID | appId | 是 | String | wxd678efh567hg6787 | 微信分配的小程序ID |
时间戳 | timeStamp | 是 | String | 1490840662 | 时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间 |
随机串 | nonceStr | 是 | String | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位。推荐随机数生成算法 |
数据包 | package | 是 | String | prepay_id=wx2017033010242291fcfe0db70013231072 | 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=wx2017033010242291fcfe0db70013231072 |
签名方式 | signType | 是 | String | MD5 | 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致 |
举例如下:
paySign = MD5(appId=wxd678efh567hg6787&nonceStr=5K8264ILTKCH16CQ2502SI8ZNMTM67VS&package=prepay_id=wx2017033010242291fcfe0db70013231072&signType=MD5&timeStamp=1490840662&key=qazwsxedcrfvtgbyhnujmikolp111111) = 22D9B4E54AB1950F51E0649E8810ACD6
调用wx.requestPayment(OBJECT)发起微信支付
Object参数说明:
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
timeStamp | String | 是 | 时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间 |
nonceStr | String | 是 | 随机字符串,长度为32个字符以下。 |
package | String | 是 | 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*** |
signType | String | 是 | 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致 |
paySign | String | 是 | 签名,具体签名方案参见微信公众号支付帮助文档; |
success | Function | 否 | 接口调用成功的回调函数 |
fail | Function | 否 | 接口调用失败的回调函数 |
complete | Function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
回调结果:
回调类型 | errMsg | 说明 |
---|---|---|
success | requestPayment:ok | 调用支付成功 |
fail | requestPayment:fail cancel | 用户取消支付 |
fail | requestPayment:fail (detail message) | 调用支付失败,其中 detail message 为后台返回的详细失败原因 |
示例代码:
wx.requestPayment(
{
'timeStamp': '',
'nonceStr': '',
'package': '',
'signType': 'MD5',
'paySign': '',
'success':function(res){},
'fail':function(res){},
'complete':function(res){}
})
本步骤在于从上步骤获取到的preppay_id作为请求参数,返回paySign参数。
有了prepay_id之后,再获取paySign(前台需要的)。这里不一定需要后台生成,前台也有相关的代码来生成。
所需要的参数:
appId,nonceStr,package,signType,timeStamp,key
这是我遇到的坑二,因为上步骤中获取到prepay_id时会给你返回一个sign,我之前以为是这个。
支付结果通知API
支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。
对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。
通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”
代码贴士
得到sign的代码:
/**
* 微信支付签名算法sign
* @param characterEncoding
* @param parameters
* @return
*/
@Test
public static String createSign(String characterEncoding,SortedMap<Object,Object> parameters){
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
Object v = entry.getValue();
if(null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + WeChatPayUtils.key);
System.out.println("字符串:"+sb.toString());
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
MD5Util工具类代码如下:
package cn.cqzdkj.utils;
import java.security.MessageDigest;
public class MD5Util {
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}
然后在需要的地方直接调用上面的createSign这个方法就行了。
SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
parameters.put("appid", appid);
parameters.put("mch_id", mch_id);
parameters.put("nonce_str",nonce_str);
parameters.put("body", body);
parameters.put("out_trade_no", "20170215");
parameters.put("total_fee", 1);
parameters.put("spbill_create_ip", "x.x.x.x");
parameters.put("notify_url","http://xxxxx.com");
parameters.put("trade_type", "JSAPI");
parameters.put("openid", "oGY_ZvxxxxxM");
parameters.put("sign","");
String characterEncoding = "UTF-8";
String mySign = createSign(characterEncoding,parameters);
System.out.println("我 的签名是:"+mySign);
小程序端完整代码如下:
/**
* 支付函数
* @param {[type]} _payInfo [description]
* @return {[type]} [description]
*/
pay:function(_payInfo,success,fail){
var payInfo = {
body:'',
total_fee:0,
order_sn:''
}
Object.assign(payInfo, _payInfo);
if(payInfo.body.length==0){
wx.showToast({
title:'支付信息描述错误'
})
return false;
}
if(payInfo.total_fee==0){
wx.showToast({
title:'支付金额不能0'
})
return false;
}
if(payInfo.order_sn.length==0){
wx.showToast({
title:'订单号不能为空'
})
return false;
}
var This = this;
This.getOpenid(function(openid){
payInfo.openid=openid;
This.request({
url:'api/pay/prepay',
data:payInfo,
success:function(res){
var data = res.data;
console.log(data);
if(!data.status){
wx.showToast({
title:data['errmsg']
})
return false;
}
This.request({
url:'api/pay/pay',
data:{prepay_id:data.data.data.prepay_id},
success:function(_payResult){
var payResult = _payResult.data;
console.log(payResult);
wx.requestPayment({
'timeStamp': payResult.timeStamp.toString(),
'nonceStr': payResult.nonceStr,
'package': payResult.package,
'signType': payResult.signType,
'paySign': payResult.paySign,
'success': function (succ) {
success&&success(succ);
},
'fail': function (err) {
fail&&fail(err);
},
'complete': function (comp) {
}
})
}
})
}
})
})
}
#参考于https://blog.csdn.net/sinat_35861727/article/details/73692794