首页 > 代码库 > [5] 微信公众号开发 - 微信支付功能开发(网页JSAPI调用)

[5] 微信公众号开发 - 微信支付功能开发(网页JSAPI调用)


1、微信支付的流程

如下三张手机截图,我们在微信网页端看到的支付,表面上看到的是 “点击支付按钮 - 弹出支付框 - 支付成功后出现提示页面”,实际上的核心处理过程是:
  • 点击支付按钮时,执行一个Ajax到后台
  • 后台通过前台的部分信息(如商品名额,金额等),将其组装成符合微信要求格式的xml,然后调用微信的“统一下单接口”
  • 调用成功后微信会返回一个组装好的xml,我们提取之中的消息(预支付id也在其中)以JSON形式返回给前台
  • 前台将该JSON传参给微信内置JS的方法中,调其微信支付
  • 支付成功后,微信会将本次支付相关信息返回给我们的服务器

这些在《微信支付官方文档 - 场景介绍》和《微信支付官方文档 - 业务流程》都进行了更详细的说明。


技术分享


2、微信支付功能开发详解 

2.1 设置支付目录和授权域名

登陆公众号,进行支付相关的目录和域名设置,详情参考《微信支付官方文档 - 开发步骤》,我这里简单贴几张官方的图就行了,这步比较简单,就不过多说明了,只提其中一点:对于微信支付授权的目录,发起微信支付的页面必须精确地位于授权目录下,假如支付页面为 http://www.a.com/wx/pay/a.html,那么授权目录必须为 http://www.a.com/wx/pay/,其他的如 http://www.a.com/wx/ , https://www.a.com/wx/pay/(http和https是不一样的),http://a.com/wx/pay/(千万别忘了www)都是不行的。填写了这些非法目录无法调起支付。

技术分享

技术分享


2.2 组装xml,调用统一下单接口,获取prepay_id

2.2.1 组装xml

点击支付按钮后,写一个Ajax将前台部分信息发送给后台,然后组装xml,调用统一下单接口。该接口在《微信支付官方文档 - 统一下单》进行了很详细的解释,我在这里进行部分说明:
参数说明    备注
appId    开发者应用ID,在 “开发 - 基本配置” 查看
mch_id微信支付的商户号,在 “微信支付 - 商户信息” 查看
device_info    终端设备号(门店号或收银设备ID)PC网页或公众号内支付,则传 “WEB”
body商品或支付的简单描述
trade_type可取值JSAPI,NATIVE,APP等,我们这里使用的是JSAPIJSAPI 公众号支付;NATIVE 原生扫码支付;APP app支付
nonce_str随机字符串参考算法:《微信支付官方文档 - 安全规范》
notify_url通知地址,微信支付成功后,微信服务器会发送信息到该url
out_trade_no商户系统内部订单号,由商户自定义,订单号要保持唯一性
total_fee订单总金额,单位:分
openid用户标识,用户在该公众号下的唯一身份标识
sign签名参考算法:《微信支付官方文档 - 安全规范
keyAPI密钥,在 “微信商户平台 - 账户中心 - API安全 - API密钥”

其他的都比较简单,重要的在于这两个涉及算法的参数,nonce_str 和 sign,这里说明一下:

  • nonce_str 随机字符串,用于保证签名不可预测
    • 算法:
    • 官方建议调用随机数函数生成,然后转为字符串

  • sign 签名
    • 算法:
    • 所有发送或接收的数据按参数名ASCII码从小到大排序,使用键值对形式拼接为字符串(如 key1=value1&key2=value2…)
    • ASCII码的字典排序,可以利用TreeMap帮我们自动实现
    • 将拼接好的字符串最后,再拼接上API密钥,即key,得到新的字符串
    • 将新的字符串进行MD5加密,并将加密后字符串全部转换为大写

按照以上的这些说明,进行xml的拼装,贴上我自己的测试代码(注:为方便测试,部分数据我直接写入方法了,如body、openId等):
  1. String appId = WeChatAPI.getAppID();
  2. String body = "JSAPI支付测试";
  3. String merchantId = WeChatAPI.getMerchantID();
  4. String tradeNo = String.valueOf(new Date().getTime());
  5. String nonceStr1 = SignUtil.createNonceStr();
  6. String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
  7. String openId = "okAkc0muYuSJUtvMf2UUQHnqYvM4";
  8. String totalFee = "1";
  9. TreeMap<String, String> map = new TreeMap<String, String>();
  10. map.put("appid", appId);
  11. map.put("mch_id", merchantId);
  12. map.put("device_info", "WEB");
  13. map.put("body", body);
  14. map.put("trade_type", "JSAPI");
  15. map.put("nonce_str", nonceStr1);
  16. map.put("notify_url", notifyUrl);
  17. map.put("out_trade_no", tradeNo);
  18. map.put("total_fee", totalFee);
  19. map.put("openid", openId);
  20. String sign = SignUtil.createSign(map);
  21. String xml = "<xml>" +
  22. "<appid>" + appId + "</appid>" +
  23. "<body>" + body +"</body>" +
  24. "<device_info>WEB</device_info>" +
  25. "<mch_id>" + merchantId + "</mch_id>" +
  26. "<nonce_str>" + nonceStr1 + "</nonce_str>" +
  27. "<notify_url>" + notifyUrl +"</notify_url>" +
  28. "<openid>" + openId + "</openid>" +
  29. "<out_trade_no>" + tradeNo + "</out_trade_no>" +
  30. "<total_fee>" + totalFee + "</total_fee>" +
  31. "<trade_type>JSAPI</trade_type>" +
  32. "<sign>" + sign + "</sign>" +
  33. "</xml>";

注意
  • body参数如果直接填写中文,在调用接口时会出现“签名错误”,要以ISO8859-1编码
  • 所以 String body = new String("body内容字符串".getBytes("ISO8859-1"));
  • 但即便如此,在支付完成后微信推送的“微信支付凭证”中,商品详情中的中文也依然显示的乱码

  • body参数内容如果包含中文,那么在调用接口时会出现“签名错误”
  • 在网上找了很多方法,有了如上删除线部分的方法,但是仍然是有问题的,因为支付成功后的凭证里中文是乱码
  • 后来终于在网上各种倒腾,找到了原因,确实是编码问题,但问题不在body是否使用ISO8859-1,而在MD5的加密算法中是否使用UTF-8
  • 所以 md.update(sourceStr.getBytes("UTF-8"));

两个算法的代码如下:
  1. /**
  2. * 生成随机数
  3. * <p>算法参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
  4. * @return 随机数字符串
  5. */
  6. public static String createNonceStr() {
  7. SecureRandom random = new SecureRandom();
  8. int randomNum = random.nextInt();
  9. return Integer.toString(randomNum);
  10. }
  11. /**
  12. * 生成签名,用于在微信支付前,获取预支付时候需要使用的参数sign
  13. * <p>算法参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
  14. * @param params 需要发送的所有数据设置为的Map
  15. * @return 签名sign
  16. */
  17. public static String createSign(TreeMap<String, String> params) {
  18. String signValue = "";
  19. String stringSignTemp = "";
  20. String stringA = "";
  21. //获得stringA
  22. Set<String> keys = params.keySet();
  23. for (String key : keys) {
  24. stringA += (key + "=" + params.get(key) + "&");
  25. }
  26. stringA = stringA.substring(0, stringA.length() - 1);
  27. //获得stringSignTemp
  28. stringSignTemp = stringA + "&key=" + WeChatAPI.getMerchantKey();
  29. //获得signValue
  30. signValue = encryptByMD5(stringSignTemp).toUpperCase();
  31. log.debug("预支付签名:" + signValue);
  32. return signValue;
  33. }
  34. /**
  35. * MD5加密
  36. *
  37. * @param sourceStr
  38. * @return
  39. */
  40. public static String encryptByMD5(String sourceStr) {
  41. String result = "";
  42. try {
  43. MessageDigest md = MessageDigest.getInstance("MD5");
  44. md.update(sourceStr.getBytes("UTF-8"));
  45. byte b[] = md.digest();
  46. int i;
  47. StringBuffer buf = new StringBuffer("");
  48. for (int offset = 0; offset < b.length; offset++) {
  49. i = b[offset];
  50. if (i < 0)
  51. i += 256;
  52. if (i < 16)
  53. buf.append("0");
  54. buf.append(Integer.toHexString(i));
  55. }
  56. result = buf.toString();
  57. } catch (NoSuchAlgorithmException e) {
  58. System.out.println(e);
  59. }
  60. return result;
  61. }

2.2.2 调用统一下单接口,获取prepay_id

有了组装好的xml,现在我们直接使用POST方式的请求发送给微信提供的接口就可以了,如果一切顺利,我们会收到微信返回的xml字符串,格式示例如下:
  1. <xml>
  2. <return_code><![CDATA[SUCCESS]]></return_code>
  3. <return_msg><![CDATA[OK]]></return_msg>
  4. <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
  5. <mch_id><![CDATA[10000100]]></mch_id>
  6. <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
  7. <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
  8. <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
  9. <result_code><![CDATA[SUCCESS]]></result_code>
  10. <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
  11. <trade_type><![CDATA[JSAPI]]></trade_type>
  12. </xml>

其中我们最需要的就是 prepay_id,这个值在后续需要用到。这段过程比较简单,其中提取prepay_id我是用的正则,我直接贴代码好了:
  1. String url = WeChatAPI.getUrl_prePay();
  2. String result = NetUtil.sendRequest(url, "POST", xml);
  3. String reg = "<prepay_id><!\\[CDATA\\[(\\w+)\\]\\]></prepay_id>";
  4. Pattern pattern = Pattern.compile(reg);
  5. Matcher matcher = pattern.matcher(result);
  6. String prepayId = "";
  7. while (matcher.find()) {
  8. prepayId = matcher.group(1);
  9. log.debug("预支付ID:" + prepayId);
  10. }


2.3 回传参数,调起微信支付JS

2.3.1 回传参数

这时候,已经有了预支付ID,但是后台的处理还没有结束,我们还没有把该有的信息返回给前台。那么前台需要哪些东西呢?《微信支付官方文档 - 微信内H5调起支付》有详细的解释,这里再贴一下:
参数    说明    备注
appId开发者应用ID,在 “开发 - 基本配置” 查看
timeStamp    时间戳,标准北京时间,秒级(10位数字)
nonceStr    随机字符串参考算法:《微信支付官方文档 - 安全规范
package    订单详情扩展字符串,其实就是预支付ID示例: prepay_id=***
signType    签名方式,暂支持MD5
paySign    签名参考算法:《微信支付官方文档 - 安全规范

有了之前的经验,想必到这里对这些的获取已经没有什么问题了,但是仍然有几个注意的地方:
  • package的值是 “prepay_id=***”,而不是 "***" 的方式(***表示之前获取的prepay_id)
  • timeStamp注意使用标准北京时间,可以使用Calendar设置Locale为CHINA,因为是秒级所以记得除以1000
  • paySign签名要重新生成,算法还是之前的,但是参数需要除自己以外的 appId、timeStamp、nonceStr、package、signType
  • 之前xml中参数appid是小写,这里的appId是大写的I

好了,因为前台接受到参数以后会以JSON的形式发送给微信服务器,所以我们这里后台,直接就把这些参数封装到一个JSONObject中就行了,然后转成JSON的形式发给前台。下面贴一下我的测试代码,签名算法和之前一样,我这里就不重复贴出来了:
  1. String url = WeChatAPI.getUrl_prePay();
  2. String result = NetUtil.sendRequest(url, "POST", xml);
  3. String reg = "<prepay_id><!\\[CDATA\\[(\\w+)\\]\\]></prepay_id>";
  4. Pattern pattern = Pattern.compile(reg);
  5. Matcher matcher = pattern.matcher(result);
  6. String prepayId = "";
  7. while (matcher.find()) {
  8. prepayId = matcher.group(1);
  9. log.debug("预支付ID:" + prepayId);
  10. }
  11. Date beijingDate = Calendar.getInstance(Locale.CHINA).getTime();
  12. String nonceStr2 = SignUtil.createNonceStr();
  13. JSONObject json = new JSONObject();
  14. json.put("appId", appId);
  15. json.put("timeStamp", beijingDate.getTime() / 1000);
  16. json.put("nonceStr", nonceStr2);
  17. json.put("package", "prepay_id=" + prepayId);
  18. json.put("signType", "MD5");
  19. TreeMap<String, String> map2 = new TreeMap<String, String>();
  20. map2.put("appId", appId);
  21. map2.put("timeStamp", String.valueOf(beijingDate.getTime() / 1000));
  22. map2.put("nonceStr", nonceStr2);
  23. map2.put("package", "prepay_id=" + prepayId);
  24. map2.put("signType", "MD5");
  25. String paySign = SignUtil.createSign(map2);
  26. json.put("paySign", paySign);
  27. String re = json.toJSONString();
  28. AjaxSupport.sendSuccessText(null, re);

2.3.2 使用微信内置的JS调起微信支付

前台的调用就很简单了,看下官方给的示例代码:
  1. function onBridgeReady(){
  2. WeixinJSBridge.invoke(
  3. ‘getBrandWCPayRequest‘, {
  4. "appId":"wx2421b1c4370ec43b", //公众号名称,由商户传入
  5. "timeStamp":"1395712654", //时间戳,自1970年以来的秒数
  6. "nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串
  7. "package":"prepay_id=u802345jgfjsdfgsdg888",
  8. "signType":"MD5", //微信签名方式:
  9. "paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名
  10. },
  11. function(res){
  12. if(res.err_msg == "get_brand_wcpay_request:ok" ) {} // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
  13. }
  14. );
  15. }
  16. if (typeof WeixinJSBridge == "undefined"){
  17. if( document.addEventListener ){
  18. document.addEventListener(‘WeixinJSBridgeReady‘, onBridgeReady, false);
  19. }else if (document.attachEvent){
  20. document.attachEvent(‘WeixinJSBridgeReady‘, onBridgeReady);
  21. document.attachEvent(‘onWeixinJSBridgeReady‘, onBridgeReady);
  22. }
  23. }else{
  24. onBridgeReady();
  25. }

使用时直接替换掉invoke方法中的参数即可,实际上如果后台直接是传递的JSON字符串到前台,可以直接解析为JS对象作为参数,下面贴我自己的代码:
  1. $().invoke("/pay/do/pay.q", null, function (re) {
  2. var result = JSON.parse(re);
  3. function onBridgeReady(){
  4. WeixinJSBridge.invoke(
  5. ‘getBrandWCPayRequest‘, result, function(res){
  6. alert(JSON.stringify(res));
  7. if(res.err_msg == "get_brand_wcpay_request:ok" ) {
  8. //doit 这里处理支付成功后的逻辑,通常为页面跳转
  9. }
  10. }
  11. );
  12. }
  13. if (typeof WeixinJSBridge == "undefined"){
  14. if( document.addEventListener ){
  15. document.addEventListener(‘WeixinJSBridgeReady‘, onBridgeReady, false);
  16. }else if (document.attachEvent){
  17. document.attachEvent(‘WeixinJSBridgeReady‘, onBridgeReady);
  18. document.attachEvent(‘onWeixinJSBridgeReady‘, onBridgeReady);
  19. }
  20. }else{
  21. onBridgeReady();
  22. }
  23. });

另外,在这个页面调试有个小技巧,将微信回调的JS对象序列化为JSON字符串,进行弹窗显示:alert(JSON.stringify(res));
技术分享
 

2.4 校验信息的正确性

实际上在完成上面的步骤以后,已经可以进行微信支付了。这最后一步主要是为了确认支付信息的正确性,以及传递给我们本次支付的一些信息,以便业务处理。

支付成功后,微信会将本次支付的相关信息,以流的方式发送给我们指定的url地址,而我们指定的url地址,就是第一次组装xml时 <notify_url> 中填写的地址,下面我们可以先回顾一下:
  1. ...
  2. String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
  3. ...
  4. String xml = "<xml>" +
  5. "<appid>" + appId + "</appid>" +
  6. "<body>" + body +"</body>" +
  7. "<device_info>WEB</device_info>" +
  8. "<mch_id>" + merchantId + "</mch_id>" +
  9. "<nonce_str>" + nonceStr1 + "</nonce_str>" +
  10. "<notify_url>" + notifyUrl +"</notify_url>" +
  11. "<openid>" + openId + "</openid>" +
  12. "<out_trade_no>" + tradeNo + "</out_trade_no>" +
  13. "<total_fee>" + totalFee + "</total_fee>" +
  14. "<trade_type>JSAPI</trade_type>" +
  15. "<sign>" + sign + "</sign>" +
  16. "</xml>";

而我们要做的,就是接受到这些信息后,进行处理,并对微信服务器做出应答。如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。详情请参考《微信支付官方文档 - 支付结果通知》

对于这部分,微信推荐我们的做法是:当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。另,商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。


...
(待续,这部分还没有进行代码测试,等测试完了再把博文补完,实际上,核心的支付功能已经写完了)



3、参考链接

  • 微信支付之JSAPI开发第一篇-基本概念
  • 微信公众号支付开发全过程 --JAVA



[5] 微信公众号开发 - 微信支付功能开发(网页JSAPI调用)