目录
一、JSAPI支付概述
微信官方提供了很多种支付方式,如扫码支付、JSAPI支付、H5支付、NATIVE支付等等,支付接入文档地址:https://pay.weixin.qq.com/wiki/doc/api/index.html。在这几种支付方式中,JSAPI支付应该是稍微复杂点的支付,配置以及流程相对复杂,本文总结一下JSAPI支付的详细实现步骤。
JSAPI支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:
- ◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付;
- ◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付;
- ◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付;
注意:JSAPI支付只能用微信浏览器打开。如果需要使用外部浏览器,可以对接H5支付这种方式。
JSAPI支付交互细节:
(1)用户打开商户网页选购商品,发起支付,在网页通过JavaScript调用getBrandWCPayRequest接口,发起微信支付请求,用户进入支付流程。
(2)用户成功支付点击完成按钮后,商户的前端会收到JavaScript的返回值。商户可直接跳转到支付成功的静态页面进行展示。
(3)商户后台收到来自微信开放平台的支付成功回调通知,标志该笔订单支付成功。
注:(2)和(3)的触发不保证遵循严格的时序。JS API返回值作为触发商户网页跳转的标志,但商户后台应该只在收到微信后台的支付成功回调通知后,才做真正的支付成功的处理。
二、名词解释
- 签名
商户后台和微信支付后台根据相同的密钥和算法生成一个结果,用于校验双方身份合法性。签名的算法由微信支付制定并公开,常用的签名方式有:MD5、SHA1、SHA256、HMAC等。
- JSAPI网页支付
JSAPI网页支付即前文说的公众号支付,可在微信公众号、朋友圈、聊天会话中点击页面链接,或者用微信“扫一扫”扫描页面地址二维码在微信中打开商户HTML5页面,在页面内下单完成支付。
- Openid
用户在公众号内的身份标识,不同公众号拥有不同的openid。商户后台系统通过登录授权、支付通知、查询订单等API可获取到用户的openid。主要用途是判断同一个用户,对用户发送客服消息、模版消息等。企业号用户需要使用企业号userid转openid接口将企业成员的userid转换成openid。
三、签名算法
签名生成的通用步骤如下:
第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
特别注意以下重要规则:
- ◆ 参数名ASCII码从小到大排序(字典序);
- ◆ 如果参数的值为空不参与签名;
- ◆ 参数名区分大小写;
- ◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
- ◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段
第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。
◆ key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
注意:参与签名的key是微信商户平台支付的API密钥,并不是微信公众平台的appSecret,否则会报"签名错误",API密钥设置路径:微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->密钥设置,如下图所示:
在调试的时候,我们可以使用https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1提供的签名校验工具,此工具旨在帮助开发者检测调用【微信支付接口API】时发送的请求参数中生成的签名是否正确,提交相关信息后可获得签名校验结果。
具体使用参照下图:
签名常见问题:
- 注意参数是否区分大小写,参数大小写不正确将会导致签名错误;
- 检查所有参数是否与文档完全一致;
- 请求数据的编码是否正确,微信支付接口编码要求统一为UTF-8;
- 签名原串是否存在被URLencode编码的参数,微信支付的签名原串要求使用参数的原值进行签名;
- 请求参数是否存在特殊字符,或者字段长度不符的情况;
四、设置支付目录和授权域名
【a】微信商户平台设置支付目录
支付授权目录说明:
- 1、商户最后请求拉起微信支付收银台的页面地址我们称之为“支付目录”,例如:https://www.weixin.com/pay.php;
- 2、商户实际的支付目录必须和在微信支付商户平台设置的一致,否则会报错“当前页面的URL未注册:”;
支付授权目录设置说明:
登录微信支付商户平台(pay.weixin.qq.com)-->产品中心-->开发配置,设置后一般5分钟内生效。
支付授权目录校验规则说明:
1、如果支付授权目录设置为顶级域名(例如:https://www.weixin.com/ ),那么只校验顶级域名,不校验后缀;
2、如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为https://www.weixin.com/abc/123/,则实际请求页面目录不能为https://www.weixin.com/abc/,也不能为https://www.weixin.com/abc/123/pay/,必须为https://www.weixin.com/abc/123/
下图是笔者的配置,可以看到开放了:http://wwtt.utools.club/ 这个支付目录。
JSAPI支付在请求支付的时候会校验请求来源是否有在商户平台做了配置,所以必须确保支付目录已经正确的被配置,否则将验证失败,请求支付不成功。
【b】微信公众平台设置授权域名
开发JSAPI支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名,只有被设置过的域名才是一个有效的获取openid的域名,否则将获取失败。具体界面如图所示:
网页授权域名是用来向微信获取code,根据code获取openId和access_token。
五、JSAPI支付流程
微信内网页支付时序图如下所示:
大体流程步骤如下:
1. 商户后台系统生成图文支付消息或二维码;
2. 用户点击消息或者扫描二维码在微信浏览器打开我们的业务H5网页;
3. 微信客户端网页内请求生成支付订单;
4. 商户后台系统生成商户订单;
5. 商户后台系统调用统一下单API, 微信支付系统生成预付单,并返回预付单信息(prepay_id)等信息;
6. 商户后台系统生成JSAPI页面调用的支付参数并签名,返回支付参数prepay_id、paySign等给微信客户端;
7. 用户点击发起支付,微信客户端JSAPI接口请求微信支付系统支付;
8. 微信支付系统检查参数是否合法和授权域权限等信息,并返回验证结果,并要求支付授权;
9. 微信客户端提示输入密码,用户确认支付,输入支付密码;
10. 微信支付系统验证授权,异步通知商户后台系统支付结果;
11. 商户后台系统告知微信支付系统,微信通知处理结果;
12. 微信支付系统返回支付结果,并发送微信消息提示,微信跳转回商户H5页面;
商户系统和微信支付系统主要交互:
- 1、商户后台(即我们的业务系统)调用统一下单接口请求订单,api参见公共api【统一下单API】
- 2、商户后台(即我们的业务系统)接收支付通知,api参见公共api【支付结果通知API】
- 3、商户后台(即我们的业务系统)查询支付结果,api参见公共api【查询订单API】
六、JSAPI支付下单
通过前面的介绍,相信小伙伴们都已经对微信支付--JSAPI方式的流程、需要获取的参数、签名的算法都有了一定的了解,下面我们就以一个案例详细说明如何实现JSAPI支付,由于需要在微信公众号内完成支付,此操作需要进行网页授权。
【a】微信网页授权
如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。
网页授权文档地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
具体而言,网页授权流程分为四步:
1、引导用户进入授权页面同意授权,获取code;
2、通过code换取网页授权access_token(与基础支持中的access_token不同);
3、如果需要,开发者可以刷新网页授权access_token,避免过期;
注意:这里由于微信 JSAPI下单需要获取openid才行,并不需要获取到用户基本信息(用户昵称,头像等信息),所以此次案例,只需要到第三步获取网页授权access_token后,微信服务器已经返回openid了,我们直接获取即可。
4、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制);
第一步:用户同意授权,获取code
引导关注者打开如下页面:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
参数说明:
下面是前端部分代码:当用户没有进行授权,引导其进入授权页面
if (!wxCode) { //如果微信支付,且未获取code值,此时需要先获取code值
this.props.dispatch({
type: 'payMain/getWxAppid',
params: { zfshPkid: zfshPkid },
callback: data => {
//data其实就是查询后台设置的appId
var urlencode = require('urlencode');
//redirect_uri需要进行urlencode
let location = urlencode(window.location.href);
let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${data}&redirect_uri=${location}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect`;
window.location = url;
},
});
return;
}
用户如果同意授权后,页面将跳转至 redirect_uri/?code=CODE&state=STATE。
code说明 : code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
第二步:通过code换取网页授权access_token
公众号可通过下述接口来获取网页授权access_token。如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。
请求方法:
获取code后,请求以下链接获取access_token:https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
参数说明:
正确时返回的JSON数据包如下:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE"
}
第三步:调用接口统一下单
微信支付只需要openid,获取到access_token之后只需要里面openid参数, 拿到openid之后,我们调用微信统一下单接口生成预付单。
文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
下面来看看具体的实现代码:
前端代码:
getQueryString = (name) => {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(window.location.href) || [, ''])[1].replace(/\+/g, '%20')) || null;
};
// 组件渲染后调用
componentDidMount() {
//获取用户同意授权后拼接在redirect_uri上面的code
let code = this.getQueryString('code');
if (code) {
this.setState({
wxCode: code,
payWay: '微信支付',
}, () => {
//已经获取了code,开始执行支付操作
//此事件就是确认支付按钮事件
this.handleSaveBtn();
});
}
//点击支付按钮事件
handleSaveBtn = () => {
//获取勾选的支付收费项目
//...........
if (!wxCode) { //如果微信支付,且未获取code值,此时需要先获取code值
this.props.dispatch({
type: 'payMain/getWxAppid',
params: { zfshPkid: zfshPkid },
callback: data => {
var urlencode = require('urlencode');
let location = urlencode(window.location.href);
let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${data}&redirect_uri=${location}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect`;
window.location = url;
},
});
return;
}
//需要支付的收费项目订单信息
let params = {};
//...........
//创建订单消息保存到业务系统
//业务系统保存支付订单、订单详情
//我们需要在我们的业务系统先保存此次支付的订单、订单明细,并记录日志,只有我们业务系统保存成功之后,我们才请求微信服务器发起一个预付单请求。
this.props.dispatch({
type: 'payMain/saveAliPayOrder',
params: params,
callback: data => {
if (data.code == '1') {
let orderPkid = data.data || '';
if (orderPkid) { //支付订单主键ID
//通过code去后台查询openID
this.goToWxPay(orderPkid, paymentAmount, zfshPkid, wxCode, id);
}
} else {
Toast.fail('保存订单失败,请稍后重试!');
}
},
});
}
/**
* 微信支付
*/
goToWxPay = (orderPkid, paymentAmount, zfshPkid, code, id) => {
paymentAmount = parseInt(paymentAmount * 100) + '';//转换成分
this.props.dispatch({
type: 'payMain/goToWxPay', //此处对应controller的/toWxPay接口
params: { ddid: orderPkid, paymentAmount: paymentAmount, zfshPkid: zfshPkid, code: code },
callback: data => {
if (data && data.code == 1) {
//打开微信支付页面
//................
},
});
};
后端请求接口:
请求微信下单Controller:
@ApiOperation(value = "跳转微信支付界面", notes = "跳转微信支付界面-", httpMethod = "POST")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "Map", name = "params", dataType = "Map", required = true, value = "微信支付参数信息")})
@RequestMapping(value = "/toWxPay", method = RequestMethod.POST)
public JsonResult toWxPay(@RequestBody Map<String, Object> params, HttpServletRequest request) {
try {
return JsonResult.success(wxPayService.toWxPay(params, request));
} catch (Exception e) {
logger.error(e.getMessage(), e);
return JsonResult.failure(e.getMessage());
}
}
Service业务层接口:
/**
* 微信支付添加订单
*
* @param params
* @param request
* @return
*/
Map<String, String> toWxPay(Map<String, Object> params, HttpServletRequest request) throws Exception;
请求微信下单ServiceImpl:
@Override
public Map<String, String> toWxPay(Map<String, Object> params, HttpServletRequest request) {
Map<String, String> result = new HashMap<>(20);
try {
if (null == params || params.isEmpty()) {
logger.error("【微信支付】参数缺失,请检查!");
result.put("code", "0");
result.put("msg", "参数缺失,请检查!");
return result;
}
//订单ID
String ddid = null != params.get("ddid") ? params.get("ddid").toString() : "";
//支付商号ID
String zfshPkid = null != params.get("zfshPkid") ? params.get("zfshPkid").toString() : "";
//支付金额
String paymentAmount = null != params.get("paymentAmount") ? params.get("paymentAmount").toString() : "";
//用户微信授权后获取的code,用于获取access_token和openId
String code = null != params.get("code") ? params.get("code").toString() : "";
//1、获取支付商户信息
ZfglZfshbVO zfglZfshbVO = openapiMapper.getZfshInfo(zfshPkid);
if (null == zfglZfshbVO) {
logger.error("【微信支付】支付商户信息参数缺失,请检查!");
result.put("code", "0");
result.put("msg", "支付商户信息参数缺失,请检查!");
return result;
}
//2. 根据订单ID查询是否存在未支付的订单信息
PayOrderDTO payOrderDTO = payOrderPOMapper.getOrderInfoByPkid(ddid, "1");
if (null == payOrderDTO) {
logger.error("【微信支付】暂未查询到未支付订单!");
result.put("code", "0");
result.put("msg", "暂未查询到未支付订单!");
return result;
}
//3、通过code获取openid信息
String openId = getOpenId(zfglZfshbVO.getAppid(), zfglZfshbVO.getSyxx(), code);
if (StringUtils.isBlank(openId)) {
logger.error("【微信支付】openId参数缺失,请检查!");
result.put("code", "0");
result.put("msg", "【微信支付】openId参数缺失,请检查!");
return result;
}
/**
* 生成签名参考文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3
* 签名校验工具: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1
*/
//微信商户支付API密钥,用于生成签名
String apiKey = zfglZfshbVO.getApikey();
/**
* 统一下单参考文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
*/
//4. 组装微信生成预付单所需参数
Map<String, String> paymentParamsMap = new HashMap<>(30);
//公众账号ID
paymentParamsMap.put("appid", zfglZfshbVO.getAppid());
//商户号
paymentParamsMap.put("mch_id", zfglZfshbVO.getShwyyhh());
//随机字符串,不长于32位
paymentParamsMap.put("nonce_str", WXPayUtil.generateNonceStr());
//商户订单号
paymentParamsMap.put("out_trade_no", payOrderDTO.getDdh().split("@")[0]);
//附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
Map<String, String> customParamsMap = new HashMap<>(10);
customParamsMap.put("zfshbPkid", zfshPkid);
paymentParamsMap.put("attach", JSON.toJSONString(customParamsMap));
//设备号,终端设备号(门店号或收银设备ID),注意:PC网页或公众号内支付请传"WEB"
paymentParamsMap.put("device_info", "WEB");
//货币类型,符合ISO 4217标准的三位字母代码,默认人民币:CNY
paymentParamsMap.put("fee_type", "CNY");
//总金额,订单总金额,单位为分
paymentParamsMap.put("total_fee", paymentAmount);
//商品简单描述,该字段须严格按照规范传递
paymentParamsMap.put("body", zfglZfshbVO.getMc() + "-微信支付");
//终端IP
paymentParamsMap.put("spbill_create_ip", WXPayUtil.getIpAddress(request));
//异步通知地址
paymentParamsMap.put("notify_url", zfglZfshbVO.getYbtzurl());
//交易类型,JSAPI -JSAPI支付 NATIVE -Native支付 APP -APP支付
paymentParamsMap.put("trade_type", "JSAPI");
//用户标识, trade_type=JSAPI时(即JSAPI支付),此参数必传,此参数为微信用户在商户对应appid下的唯一标识。
paymentParamsMap.put("openid", openId);
//场景信息
paymentParamsMap.put("scene_info", "{\"h5_info\": {\"type\":\"Wap\",\"wap_url\": \"\",\"wap_name\": \"微信支付\"}}");
//生成签名,MD5方式
paymentParamsMap.put("sign", WXPayUtil.generateSignature(paymentParamsMap, apiKey, WXPayConstants.SignType.MD5));
String payInfoXml = WXPayUtil.mapToXml(paymentParamsMap);
logger.info("【微信支付】支付参数XML: {}", payInfoXml);
//5.发送POST请求微信生成预付单
String response = HttpUtil.doPost("https://api.mch.weixin.qq.com/pay/unifiedorder", payInfoXml, "UTF-8", 3000, 5000);
if (StringUtils.isBlank(response)) {
logger.error("【微信支付】请求微信生成预付单失败!");
result.put("code", "0");
result.put("msg", "请求微信生成预付单失败!");
return result;
}
Map<String, String> resMap = WXPayUtil.xmlToMap(response);
String returnCode = resMap.get("return_code");
//解析微信返回的支付预付单信息,注意需要二次签名.
if (SUCCESS.equalsIgnoreCase(returnCode)) {
String resultCode = resMap.get("result_code");
if (SUCCESS.equalsIgnoreCase(resultCode)) {
//参数需要重新进行签名计算,参与签名的参数为:appId、timeStamp、nonceStr、package、signType,参数区分大小写。
result.put("appId", zfglZfshbVO.getAppid());
result.put("timeStamp", WXPayUtil.getCurrentTimestamp() + "");
result.put("nonceStr", resMap.get("nonce_str"));
result.put("package", "prepay_id=" + resMap.get("prepay_id"));
result.put("signType", WXPayConstants.MD5);
//第二次签名方式跟第一次一致,也是需要传入API密钥参与生成
String paySign = WXPayUtil.generateSignature(result, apiKey, WXPayConstants.SignType.MD5);
result.put("paySign", paySign);
result.put("code", "1");
return result;
} else {
logger.error("【微信支付失败】>> errMsg: {}", resMap.get("err_code_des"));
result.put("code", "0");
result.put("msg", resMap.get("err_code_des"));
}
} else {
logger.error("【微信支付失败】>> errMsg: {}", resMap.get("return_msg"));
result.put("code", "0");
result.put("msg", resMap.get("return_msg"));
}
} catch (Exception e) {
logger.error("【微信支付失败】: {}", e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 获取OpenId
*
* @param appId 公众号唯一标识
* @param secret 公众号秘钥
* @param code 用户同意授权后获取的code
* @return </br>
* 说明: 通过code换取网页授权access_token,由于微信支付需要openId生成预付单,此接口同时返回了openId,故直接获取即可;
* 如需获取用户信息,还需要根据access_token获取, 发送GET请求: https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
*/
@SuppressWarnings("unchecked")
private String getOpenId(String appId, String secret, String code) {
Map<String, String> map = new HashMap(10);
String url = "https://api.weixin.qq.com/sns/oauth2/access_token";
//公众号的唯一标识
map.put("appid", appId);
//公众号的appsecret
map.put("secret", secret);
//用户同意授权后获取的code参数
map.put("code", code);
//填写为authorization_code
map.put("grant_type", "authorization_code");
try {
String tokenStr = HttpUtil.doGet(url, map, "UTF-8", 3000, 5000);
Map<String, Object> codeMap = JSON.parseObject(tokenStr, Map.class);
if (null != codeMap && !codeMap.isEmpty() && codeMap.containsKey("errcode")) {
//获取openId出错
String errCode = null != codeMap.get("errcode") ? codeMap.get("errcode").toString() : "";
String errMsg = null != codeMap.get("errmsg") ? codeMap.get("errmsg").toString() : "";
logger.error("【微信支付】获取openId失败 >> errcode:{}, errmsg: {}", errCode, errMsg);
return null;
} else {
String openId = null != codeMap.get("openid") ? codeMap.get("openid").toString() : "";
return StringUtils.isNotBlank(openId) ? openId : "";
}
} catch (Exception exception) {
exception.printStackTrace();
return null;
}
}
WXPayUtil微信支付工具类:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;
public class WXPayUtil {
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
throw ex;
}
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
org.w3c.dom.Document document = WXPayXmlUtil.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
return generateSignedXml(data, key, WXPayConstants.SignType.MD5);
}
/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名类型
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
String sign = generateSignature(data, key, signType);
data.put(WXPayConstants.FIELD_SIGN, sign);
return mapToXml(data);
}
/**
* 判断签名是否正确
*
* @param xmlStr XML格式数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
Map<String, String> data = xmlToMap(xmlStr);
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key).equals(sign);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
*
* @param data Map类型数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
return isSignatureValid(data, key, WXPayConstants.SignType.MD5);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名方式
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key, signType).equals(sign);
}
/**
* 生成签名
*
* @param data 待签名数据
* @param key API密钥
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key) throws Exception {
return generateSignature(data, key, WXPayConstants.SignType.MD5);
}
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
if (WXPayConstants.SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
}
else if (WXPayConstants.SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String MD5(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 生成 HMACSHA256
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
* @throws Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 日志
* @return
*/
public static Logger getLogger() {
Logger logger = LoggerFactory.getLogger("wxpay java sdk");
return logger;
}
/**
* 获取当前时间戳,单位秒
* @return
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis()/1000;
}
/**
* 获取当前时间戳,单位毫秒
* @return
*/
public static long getCurrentTimestampMs() {
return System.currentTimeMillis();
}
/**
* 从request中获取请求方IP
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
// 若以上方式均为获取到ip则证明获得客户端并没有采用反向代理直接使用getRemoteAddr()获取客户端的ip地址
ip = request.getRemoteAddr();
}
// 多个路由时,取第一个非unknown的ip
final String[] arr = ip.split(",");
for (final String str : arr) {
if (!"unknown".equalsIgnoreCase(str)) {
ip = str;
break;
}
}
return ip;
}
}
WXPayXmlUtil解析XML工具类:
import org.w3c.dom.Document;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* 2018/7/3
*/
public final class WXPayXmlUtil {
public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
return documentBuilderFactory.newDocumentBuilder();
}
public static Document newDocument() throws ParserConfigurationException {
return newDocumentBuilder().newDocument();
}
}
WXPayConstants常量类:
import org.apache.http.client.HttpClient;
/**
* 常量
*/
public class WXPayConstants {
public enum SignType {
MD5, HMACSHA256
}
public static final String DOMAIN_API = "api.mch.weixin.qq.com";
public static final String DOMAIN_API2 = "api2.mch.weixin.qq.com";
public static final String DOMAIN_APIHK = "apihk.mch.weixin.qq.com";
public static final String DOMAIN_APIUS = "apius.mch.weixin.qq.com";
public static final String FAIL = "FAIL";
public static final String SUCCESS = "SUCCESS";
public static final String HMACSHA256 = "HMAC-SHA256";
public static final String MD5 = "MD5";
public static final String FIELD_SIGN = "sign";
public static final String FIELD_SIGN_TYPE = "sign_type";
public static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9";
public static final String USER_AGENT = WXPAYSDK_VERSION +
" (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") +
") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion();
public static final String MICROPAY_URL_SUFFIX = "/pay/micropay";
public static final String UNIFIEDORDER_URL_SUFFIX = "/pay/unifiedorder";
public static final String ORDERQUERY_URL_SUFFIX = "/pay/orderquery";
public static final String REVERSE_URL_SUFFIX = "/secapi/pay/reverse";
public static final String CLOSEORDER_URL_SUFFIX = "/pay/closeorder";
public static final String REFUND_URL_SUFFIX = "/secapi/pay/refund";
public static final String REFUNDQUERY_URL_SUFFIX = "/pay/refundquery";
public static final String DOWNLOADBILL_URL_SUFFIX = "/pay/downloadbill";
public static final String REPORT_URL_SUFFIX = "/payitil/report";
public static final String SHORTURL_URL_SUFFIX = "/tools/shorturl";
public static final String AUTHCODETOOPENID_URL_SUFFIX = "/tools/authcodetoopenid";
// sandbox
public static final String SANDBOX_MICROPAY_URL_SUFFIX = "/sandboxnew/pay/micropay";
public static final String SANDBOX_UNIFIEDORDER_URL_SUFFIX = "/sandboxnew/pay/unifiedorder";
public static final String SANDBOX_ORDERQUERY_URL_SUFFIX = "/sandboxnew/pay/orderquery";
public static final String SANDBOX_REVERSE_URL_SUFFIX = "/sandboxnew/secapi/pay/reverse";
public static final String SANDBOX_CLOSEORDER_URL_SUFFIX = "/sandboxnew/pay/closeorder";
public static final String SANDBOX_REFUND_URL_SUFFIX = "/sandboxnew/secapi/pay/refund";
public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX = "/sandboxnew/pay/refundquery";
public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill";
public static final String SANDBOX_REPORT_URL_SUFFIX = "/sandboxnew/payitil/report";
public static final String SANDBOX_SHORTURL_URL_SUFFIX = "/sandboxnew/tools/shorturl";
public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid";
}
第四步:微信内H5调起支付
在微信浏览器里面打开H5网页中执行JS调起支付。接口输入输出数据格式为JSON。
注意:WeixinJSBridge内置对象在其他浏览器中无效。
getBrandWCPayRequest参数以及返回值定义:
1、网页端接口请求参数列表(参数需要重新进行签名计算,参与签名的参数为:appId、timeStamp、nonceStr、package、signType,参数区分大小写。)
如下代码是微信成功生成预付单并返回到商户系统之后的回调: 请求打开微信支付弹窗进行支付。
if (data && data.code == 1) {
//打开微信支付页面
let resData = {};
resData['appId'] = data.appId; //公众号名称,由商户传入
resData['timeStamp'] = data.timeStamp; //时间戳,自1970年以来的秒数
resData['nonceStr'] = data.nonceStr; //随机串
resData['package'] = data.package; //统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
resData['signType'] = data.signType; //微信签名方式
resData['paySign'] = data.paySign; //微信签名
if (typeof WeixinJSBridge == 'undefined') {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', function() {
this.onBridgeReady(resData, id);
}, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', function() {
this.onBridgeReady(resData, id);
});
document.attachEvent('onWeixinJSBridgeReady', function() {
this.onBridgeReady(resData, id);
});
}
} else {
this.onBridgeReady(resData, id);
}
} else {
Toast.fail(data.msg);
}
//微信内H5调起支付
onBridgeReady = (data, id) => {
//注意: WeixinJSBridge内置对象在其他浏览器中无效
//网页端接口请求参数列表(参数需要重新进行签名计算,参与签名的参数为:appId、timeStamp、nonceStr、package、signType,参数区分大小写。)
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{
'appId': data.appId, //公众号id,由商户传入
'timeStamp': data.timeStamp, //时间戳,自1970年以来的秒数
'nonceStr': data.nonceStr, //随机串
'package': data.package, //统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
'signType': data.signType, //微信签名方式
'paySign': data.paySign, //微信签名
},
function(res) {
/**
* 返回结果值说明:
* get_brand_wcpay_request:ok 支付成功
* get_brand_wcpay_request:cancel 支付过程中用户取消
* get_brand_wcpay_request:fail 支付失败
*/
if (res.err_msg === 'get_brand_wcpay_request:ok') {
Toast.success('支付成功', 1, () => {
window.location = `${window.location.origin}/#/UnifiedPaymentPlatform/payMain?id=${id}`;
});
} else {
Toast.fail('用户支付遇到错误或者取消支付');
}
},
);
};
七、支付结果通知
参考文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8
应用场景:
支付完成后,微信会把相关支付结果及用户信息通过数据流的形式发送给商户,商户需要接收处理,并按文档规范返回应答。首先我们会在配置支付请求参数的时候,配置了回调地址(notify_url),支付完成微信服务器就会将结果推送到这个地址上,我们只是需要获取那些数据就可以了,然后根据处理相关业务。
注意:
- 1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
- 2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
- 3、在订单状态不明或者没有收到微信支付结果通知的情况下,建议商户主动调用微信支付【查询订单API】确认订单状态。
特别提醒:
- 1、商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
- 2、当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
接口链接:
该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。
通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”。
微信支付异步通知接口代码:
/**
* 微信支付异步通知结果
*
* @return 说明:
* 支付完成后,微信会把相关支付结果及用户信息通过数据流的形式发送给商户,商户需要接收处理,并按文档规范返回应答。
* <p>
* 注意:
* 1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
* <p>
* 2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
* <p>
* 3、在订单状态不明或者没有收到微信支付结果通知的情况下,建议商户主动调用微信支付【查询订单API】确认订单状态。
*/
@Override
@Transactional
@SuppressWarnings("unchecked")
public String wxPaySyncNotification(HttpServletRequest request, HttpServletResponse response) {
try {
ServletInputStream servletInputStream = request.getInputStream();
BufferedReader in;
StringBuilder result = new StringBuilder();
in = new BufferedReader(new InputStreamReader(servletInputStream));
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
//1. 解析微信异步通知结果
Map<String, String> resultMap = WXPayUtil.xmlToMap(result.toString());
if (null == resultMap || resultMap.isEmpty()) {
return returnFailInfo("异步通知参数不能为空!");
}
logger.info("【微信支付】异步通知, 通知参数: {}", resultMap);
//2. 验证return_code是否为SUCCESS
String returnCode = resultMap.get("return_code");
if (SUCCESS.equalsIgnoreCase(returnCode)) {
//3. 获取微信下单时传递的自定义参数(支付商户表PKID)
String attach = resultMap.get("attach");
Map<String, Object> attachMap = (Map<String, Object>) JSON.parse(attach);
String zfshbPkid = null != attachMap.get("zfshbPkid") ? attachMap.get("zfshbPkid").toString() : "";
if (StringUtils.isBlank(zfshbPkid)) {
logger.error("微信支付自定义参数attach不能为空");
return returnFailInfo("微信支付自定义参数attach不能为空");
}
//4. 根据支付商户PKID查询相关设置信息
ZfglZfshbVO zfshInfo = openapiMapper.getZfshInfo(zfshbPkid);
if (null == zfshInfo) {
return returnFailInfo("支付商户信息为空");
}
//商户订单号
String outTradeNo = resultMap.get("out_trade_no");
//5、验证商户订单号是否存在
if (StringUtils.isBlank(outTradeNo)) {
logger.error("out_trade_no(商户订单号)不能为空");
return returnFailInfo("out_trade_no(商户订单号)不能为空");
}
//6. 查询业务系统订单信息
String ddh = openapiMapper.selectDdhByWxOutTradeNo(outTradeNo);
PayOrderPO payOrderPO = payOrderPOMapper.getPayOrderDetailByTradeNo(ddh);
if (null == payOrderPO) {
//订单不存在,返回success,反馈微信不需要继续异步通知
return returnSuccessInfo("订单不存在,无需通知");
}
//7. 判断订单是否是等待付款状态,如果不是则不需要通知
if (!"1".equals(payOrderPO.getJyzt())) {
return returnSuccessInfo("交易状态非等待付款,无需通知");
}
//8. 记录通知日志
//.......
//商户号
String mchId = resultMap.get("mch_id");
//9. 验证商户号是否存在
if (StringUtils.isBlank(mchId)) {
logger.error("mch_id(商户号)不能为空");
return returnFailInfo("mch_id(商户号)不能为空");
}
//订单总金额,单位为分
String totalFee = resultMap.get("total_fee");
if (StringUtils.isBlank(totalFee)) {
logger.error("totalFee(订单总金额)不能为空");
return returnFailInfo("totalFee(订单总金额)不能为空");
}
String shwyyhh = zfshInfo.getShwyyhh();
//10. 判断订单里面的商户号与交易金额是否与微信通知过来的商户号、交易金额相同
BigDecimal jyje = payOrderPO.getJyje();
//乘以100(单位:分)
BigDecimal newJyje = jyje.multiply(new BigDecimal(100));
if (mchId.equals(shwyyhh) && String.valueOf(newJyje.doubleValue()).equals(String.valueOf(Double.parseDouble(totalFee)))) {
//11. 根据订单ID查询出对应的订单明细
//.............
//12. 查询订单是否已通知成功
String jyzt = payOrderPOMapper.getJyzt(payOrderPO.getPkid());
if (!(StringUtils.isNotBlank(jyzt) && "3".equals(jyzt))) {
//交易支付成功,更新该订单状态为已支付
//..................
logger.info("【微信支付】交易支付成功,修改订单为已支付");
//修改订单状态
//..................
//批量保存实收数据
//..................
//批量保存实收数据明细
//..................
}
}
return returnSuccessInfo("微信支付成功");
} else {
logger.error("【微信异步通知】>>> return_code为false");
return returnFailInfo("【微信异步通知】>>> return_code为false");
}
} catch (Exception e) {
e.printStackTrace();
logger.error("微信支付回调出现未知错误");
return returnFailInfo("微信支付回调出现未知错误");
}
}
private String returnSuccessInfo(String returnMsg) {
try {
Map<String, String> map = new HashMap<>();
map.put("return_code", SUCCESS);
map.put("return_msg", returnMsg);
return WXPayUtil.mapToXml(map);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private String returnFailInfo(String returnMsg) {
try {
Map<String, String> map = new HashMap<>();
map.put("return_code", FAIL);
map.put("return_msg", returnMsg);
return WXPayUtil.mapToXml(map);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
测试结果:
由于微信异步通知接口等都需要外网可以访问,所以这里介绍一个内网穿透的工具utools来进行测试,可以免费获得一个外网访问的域名,通过映射我们本地启动的项目,进行微信开发调试。如下图所示:
支付结果测试如下图:
八、支付常见问题
下面分享一些笔者调试遇到的几个问题,并给出了相应的解决方法,当然可能还有其他原因导致,得具体问题具体分析。
【a】{"errcode":40125,"errmsg":"invalid appsecret, view more at http:\/\/t.cn\/RAEkdVq, hints: [ req_id: GhFEBK4FE-.yDWiA ]"}
如果报以上错误,我们需要检查一下appsecret是否正确,如果不正确,需要到微信公众平台重置一下。
【b】<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名错误]]></return_msg></xml>
笔者调试时遇到这个错误,是因为参与签名的key写错了,应该是微信商户平台设置的API密钥才对。
【c】errMsg: "the permission value is offline verifying"
如果报以上错误,可能是因为微信开发者工具调取不了支付弹窗,我们可以使用真机进行测试即可。
【d】当前页面的URL未注册:http://wwtt.utools.club/
如果报上述错误,说明我们在微信商户凭条配置的授权域名有问题,需要跟实际支付的域名保持一致。
【e】调用支付JSAPI缺少参数:appId
笔者调试时遇到这个错误,真的是微信的大坑。如下图说明:
上述第二个参数文档说要是json,然后笔者前端一开始使用:JSON.stringfy(params)传递进去,直接就报错缺少appId,明明已经传了还说缺失。
后来,在下面的文章中找到解决方法:https://www.cnblogs.com/danlis/p/5566740.html
居然是JSON格式问题,后来直接写成下面的写法,直接传一个对象过去,就成功弹出支付页面:
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{
'appId': data.appId, //公众号id,由商户传入
'timeStamp': data.timeStamp, //时间戳,自1970年以来的秒数
'nonceStr': data.nonceStr, //随机串
'package': data.package, //统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
'signType': data.signType, //微信签名方式
'paySign': data.paySign, //微信签名
},
function(res) {
/**
* 返回结果值说明:
* get_brand_wcpay_request:ok 支付成功
* get_brand_wcpay_request:cancel 支付过程中用户取消
* get_brand_wcpay_request:fail 支付失败
*/
if (res.err_msg === 'get_brand_wcpay_request:ok') {
Toast.success('支付成功', 1, () => {
window.location = `${window.location.origin}/#/UnifiedPaymentPlatform/payMain?id=${id}`;
});
} else {
Toast.fail('用户支付遇到错误或者取消支付');
}
},
);
【f】redirect_uri参数错误
笔者调试时遇到的问题是redirect_uri参数错误是因为获取用户授权的outh2路径写错了:
正确格式大体如下:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxf0e81c3bee622d60&redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
其他可能原因:
支付授权目录是否设置正确;
网页授权域名是否设置正确;
参数错误;
九、总结
微信JSAPI支付方式较其他方式稍复杂一些,我们总结一下大体的流程:
- 获取微信支付四大参数: 公众APPID,APPSECEPT ,微信商户平台商户ID, 微信商户平台支付API密钥;
- 在微信公众平台设置好授权域名,在微信商户平台设置好授权支付目录信息;
- 用户访问我们的业务系统,进行微信网页授权,用户同意授权后获取code;
- 先保存订单信息到业务系统中,保存成功后发起微信下单接口;
- 根据code获取openid,获取成功后调用微信统一下单接口,获取预付单信息;
- 解析微信返回的预付单信息,解析出prepay_id;
- 根据appId、timeStamp、nonceStr、package、signType进行第二次签名,返回给前台;
- 商户网页前端使用getBrandWCPayRequest接口调起微信支付,传入上一步返回的参数;
- 弹出支付弹窗,用户支付成功后,在该接口里面会返回结果,同时微信也会发送异步通知到统一下单时候填写的notify_url;
- 回调支付成功通知,通知微信支付结果,并进行我们的后续的业务处理,如修改订单状态,记录日志,生成支付记录等等。
以上就是关于微信支付之JSAPI方式详细的实现步骤,对接微信支付可能会遇到一些奇奇怪怪的问题,查阅官方文档以及度娘一般都可以解决,由于笔者水平有限,文中难免有些没考虑周全的地方,还望大家指正,相互学习,一起进步!