微信公众号H5支付遇到的那些坑
简史
官方文档说的很清楚,商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,可以调用微信支付完成下单购买的流程。
当然,最近微信支付平台也加入了纯H5支付,也就是说用户可以在微信以外的手机浏览器请求微信支付的场景唤起微信支付。
当然,今天的主角是微信公众号支付,其实也不一定非在公众号中打开,只要在微信中打开就可以使用。
实现
项目使用的springboot微服务来实现,以下都是简单的伪代码实现,具体逻辑见码云。
Main
其实就是一个初始化下单操作,前台业务逻辑在这就不展示了,这个就是接收前台参数的方法:
1 2 3 4 5 6 | @RequestMapping ( "/pay" ) public String pay(Product product,ModelMap map) { logger.info( "H5支付(需要公众号内支付)" ); String url = weixinPayService.weixinPayMobile(product); return "redirect:" +url; } |
产品实体Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * 产品订单信息 * 创建者 科帮网 * 创建时间 2017年7月27日 */ @Data @NoArgsConstructor @AllArgsConstructor public class Product implements Serializable { private static final long serialVersionUID = 1L; private String productId; // 商品ID private String subject; //订单名称 private String body; // 商品描述 private String totalFee; // 总金额(单位是分) private String outTradeNo; // 订单号(唯一) private String spbillCreateIp; // 发起人IP地址 private String attach; // 附件数据主要用于商户携带订单的自定义数据 private Short payType; // 支付类型(1:支付宝 2:微信 3:银联) private Short payWay; // 支付方式 (1:PC,平板 2:手机) private String frontUrl; // 前台回调地址 非扫码支付使用 } |
由于整合了Dubbo,使用PRC的方式调用,这里定义一个service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Override public String weixinPayMobile(Product product) { StringBuffer url = new StringBuffer(); String totalFee = product.getTotalFee(); //redirect_uri 需要在微信支付端添加认证网址 totalFee = CommonUtil.subZeroAndDot(totalFee); url.append( "http://open.weixin.qq.com/connect/oauth2/authorize?" ); url.append( "appid=" +ConfigUtil.APP_ID); url.append( "&redirect_uri=" +server_url+ "weixinMobile/dopay?" ); //注意 此处 get请求 拼接相关参数 用于redirect_uri获取 url.append("outTradeNo="+product.getOutTradeNo()+"&totalFee="+totalFee); url.append( "&response_type=code&scope=snsapi_base&state=" ); url.append( "#wechat_redirect" ); return url.toString(); } |
Topay
大家有没有注意到redirect_uri参数中,我们定义了我们自己系统中的url请求,如下:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | @RequestMapping (value = "dopay" ) public String dopay(HttpServletRequest request, HttpServletResponse response) throws Exception { //此处为weixinPayMobile方法中拼接的参数 String orderNo = request.getParameter( "outTradeNo" ); String totalFee = request.getParameter( "totalFee" ); //获取code 这个在微信支付调用时会自动加上这个参数无须设置 String code = request.getParameter( "code" ); //获取用户openID(JSAPI支付必须传openid) String openId = MobileUtil.getOpenId(code); String notify_url =server_url+ "/weixinMobile/WXPayBack" ; //回调接口 String trade_type = "JSAPI" ; // 交易类型H5支付 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); ConfigUtil.commonParams(packageParams); packageParams.put( "body" , "报告" ); // 商品描述 packageParams.put( "out_trade_no" , orderNo); // 商户订单号 packageParams.put( "total_fee" , totalFee); // 总金额 packageParams.put( "spbill_create_ip" , AddressUtils.getIpAddr(request)); // 发起人IP地址 packageParams.put( "notify_url" , notify_url); // 回调地址 packageParams.put( "trade_type" , trade_type); // 交易类型 packageParams.put( "openid" , openId); //用户openID String sign = PayCommonUtil.createSign( "UTF-8" , packageParams,ConfigUtil.API_KEY); packageParams.put( "sign" , sign); // 签名 String requestXML = PayCommonUtil.getRequestXml(packageParams); String resXml = HttpUtil.postData(ConfigUtil.UNIFIED_ORDER_URL, requestXML); Map map = XMLUtil.doXMLParse(resXml); String returnCode = (String) map.get( "return_code" ); String returnMsg = (String) map.get( "return_msg" ); StringBuffer url = new StringBuffer(); if ( "SUCCESS" .equals(returnCode)){ String resultCode = (String) map.get( "result_code" ); String errCodeDes = (String) map.get( "err_code_des" ); if ( "SUCCESS" .equals(resultCode)){ //获取预支付交易会话标识 String prepay_id = (String) map.get( "prepay_id" ); String prepay_id2 = "prepay_id=" + prepay_id; String packages = prepay_id2; SortedMap<Object, Object> finalpackage = new TreeMap<Object, Object>(); String timestamp = DateUtil.getTimestamp(); String nonceStr = packageParams.get( "nonce_str" ).toString(); finalpackage.put( "appId" , ConfigUtil.APP_ID); finalpackage.put( "timeStamp" , timestamp); finalpackage.put( "nonceStr" , nonceStr); finalpackage.put( "package" , packages); finalpackage.put( "signType" , "MD5" ); //这里很重要 参数一定要正确 狗日的腾讯 参数到这里就成大写了 //可能报错信息(支付验证签名失败 get_brand_wcpay_request:fail) sign = PayCommonUtil.createSign( "UTF-8" , finalpackage,ConfigUtil.API_KEY); url.append( "redirect:/weixinMobile/payPage?" ); url.append( "timeStamp=" +timestamp+ "&nonceStr=" + nonceStr + "&package=" + packages); url.append( "&signType=MD5" + "&paySign=" + sign+ "&appid=" + ConfigUtil.APP_ID); url.append( "&orderNo=" +orderNo+ "&totalFee=" +totalFee); } else { logger.info( "订单号:{}错误信息:{}" ,orderNo,errCodeDes); url.append( "redirect:/weixinMobile/error?code=0&orderNo=" +orderNo); //该订单已支付 } } else { logger.info( "订单号:{}错误信息:{}" ,orderNo,returnMsg); url.append( "redirect:/weixinMobile/error?code=1&orderNo=" +orderNo); //系统错误 } return url.toString(); } |
其实,以上代码就是一个认证(获取openid)、下单的过程,最终获取相关参数再重定向到pay页面,也就是我们定义的 redirect:/weixinMobile/payPage。
1 2 3 4 5 | //公众号H5支付主页 @RequestMapping (value = "payPage" ) public String pay(HttpServletRequest request, HttpServletResponse response) throws Exception { return "weixin/pay" ; } |
然后转发到pay.jsp
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | <%@ page language= "java" import = "java.util.*" pageEncoding= "UTF-8" %> <% String path = request.getContextPath(); String basePath = request.getScheme()+ "://" +request.getServerName()+ ":" +request.getServerPort()+path+ "/" ; //相关参数 String appId = request.getParameter( "appid" ); String timeStamp = request.getParameter( "timeStamp" ); String nonceStr = request.getParameter( "nonceStr" ); String packageValue = request.getParameter( "package" ); String paySign = request.getParameter( "paySign" ); String orderNo = request.getParameter( "orderNo" ); String totalFee = request.getParameter( "totalFee" ); %> <!DOCTYPE html> <html> <head> <title>微信支付</title> <meta charset= "UTF-8" /> <meta name= "viewport" content= "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name= "apple-mobile-web-app-capable" content= "yes" /> <meta name= "apple-mobile-web-app-status-bar-style" content= "black" /> <meta name= "format-detection" content= "telephone=no" /> <script type= "text/javascript" src= "<%=basePath%>static/js/jquery-1.10.2.min.js" ></script> <!-- 引入 jweixin- 1.0 . 0 .js--> <script src= "http://res.wx.qq.com/open/js/jweixin-1.0.0.js" ></script> </head> <body> <article class = "order-main " > <div class = "ph_order" > <div class = " affirm-info" > <h4 id= "orderNo" ></h4> <h3 id= "totalFee" ></h3> <div class = "detail-dl" > <dl> <dt>收款方</dt> <dd>科帮网</dd> </dl> <dl> <dt>商 品</dt> <dd id= "productName" >充值帮币</dd> </dl> </div> <div onclick= "callpay()" class = "pay-info" >立即支付</div> </div> </div> </article> </body> <script type= "text/javascript" > var orderNo = '<%=orderNo%>' ; var totalFee = '<%=totalFee%>' ; $(function(){ init(); }); function onBridgeReady(){ WeixinJSBridge.invoke( 'getBrandWCPayRequest' ,{ "appId" : "<%=appId%>" , "timeStamp" : "<%=timeStamp%>" , "nonceStr" : "<%=nonceStr%>" , "package" : "<%=packageValue%>" , "signType" : "MD5" , "paySign" : "<%=paySign%>" },function(res){ //使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。 if (res.err_msg == "get_brand_wcpay_request:ok" ){ window.location.href= "http://前台回调地址" ; } else if (res.err_msg == "get_brand_wcpay_request:cancel" ){ alert( "用户取消支付!" ); } else if (res.err_msg == "get_brand_wcpay_request:fail" ){ alert( "支付失败!" ); } }) } function callpay(){ if (typeof WeixinJSBridge == "undefined" ){ if ( document.addEventListener ){ document.addEventListener( 'WeixinJSBridgeReady' , onBridgeReady, false ); } else if (document.attachEvent){ document.attachEvent( 'WeixinJSBridgeReady' , onBridgeReady); document.attachEvent( 'onWeixinJSBridgeReady' , onBridgeReady); } } else { onBridgeReady(); } } function init(){ $( "#orderNo" ).html( "科帮网-订单编号:" +orderNo); totalFee = accDiv(totalFee, 100 ); $( "#totalFee" ).html( "¥" +totalFee); } function accDiv(arg1,arg2){ var t1= 0 ,t2= 0 ,r1,r2; try {t1=arg1.toString().split( "." )[ 1 ].length;} catch (e){} try {t2=arg2.toString().split( "." )[ 1 ].length;} catch (e){} with(Math){ r1=Number(arg1.toString().replace( "." , "" )); r2=Number(arg2.toString().replace( "." , "" )); return (r1/r2)*pow( 10 ,t2-t1); } } </script> </html> |
Notify
其实,这就是一个回调通知,用户支付成功以后,微信会通知我们后台支付状态,然后我们根据订单信息完成下一步业务逻辑。
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 | @RequestMapping (value = "WXPayBack" ) public void WXPayBack(HttpServletRequest request, HttpServletResponse response){ String resXml = "" ; try { //解析XML Map<String, String> map = MobileUtil.parseXml(request); String return_code = map.get( "return_code" ); //状态 String out_trade_no = map.get( "out_trade_no" ); //订单号 if (return_code.equals( "SUCCESS" )) { if (out_trade_no != null ) { //处理订单逻辑 logger.info( "微信手机支付回调成功订单号:{}" ,out_trade_no); resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> " ; } } else { logger.info( "微信手机支付回调失败订单号:{}" ,out_trade_no); resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> " ; } } catch (Exception e) { logger.error( "手机支付回调通知失败" ,e); resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> " ; } try { // ------------------------------ // 处理业务完毕 // ------------------------------ BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); } catch (Exception e) { e.printStackTrace(); } } |
坑
其实,当你完成集成测试的那一刻,也就没啥子坑了,相关的注意事项都在代码中有体现。
详细代码见:码云

作者: 小柒
出处: https://blog.52itstyle.vip

分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 如有问题, 可邮件(345849402@qq.com)咨询。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步