最近做了几个关于微信支付的项目,现在刚好有点时间,来总结一下容易踩的坑。
首先微信支付主要有这几种方式(微信公众号支付、微信H5支付、微信扫码支付、微信APP支付)
所有支付的第一步都是请求统一下单,统一下单,统一下单,请求URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder。统一下单的目的是拿到预支付交易会话标识prepay_id,这个是必须的。所有的支付调用都是通过prepay_id来识别。
还有一个重要的就是,微信支付接口签名校验工具(网址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1),此工具旨在帮助开发者检测调用【微信支付接口API】时发送的请求参数中生成的签名是否正确,提交相关信息后可获得签名校验结果。签名正确基本就没什么问题了。
一、微信资料准备
1、注册一个微信公众号,并年审(年审需要交300费用,然后等待微信那边审核通过),这个跟着微信步骤弄就行了
微信公众平台(https://mp.weixin.qq.com/)
2、注册微信商户号,成为微信商家
微信支付(https://pay.weixin.qq.com/)
3、把公众号与商户号绑定
4、需要用到的信息位置
①、公众号(AppID)
②、商户号(MCH_ID,API_KEY)
③、商户授权目录、扫码回调地址
④、商户号关联的APPID
⑤、商户号支付功能开通
二、微信公众号支付
微信公众号支付需要的参数有:APP_ID(微信公众号开发者ID)、APP_SECRET(微信公众号开发者密码)、MCH_ID(商户ID)、API_KEY(商户密钥)。
微信公众号支付应用的场景是在微信内部的H5环境中是用的支付方式。因为要通过网页授权获取用户的openId,所以必须要配置网页授权域名。同时要配置JS接口安全域名。
1、工具类
1 /** 2 * 公众号支付配置信息 3 */ 4 public class PayConfigUtil { 5 public static String APP_ID="公众号APPID"; 6 public static String MCH_ID="商户号"; 7 public static String API_KEY="API密钥"; 8 public static String CREATE_IP="127.0.0.1"; 9 public static String NOTIFY_URL="微信回调地址";//公众号支付回调-与商户里配置的支付回调无关 10 //微信统一下单地址 11 public static String UFDODER_URL="https://api.mch.weixin.qq.com/pay/unifiedorder"; 12 }
1 /** 2 * 微信支付常用方法 3 */ 4 public class PayCommonUtil { 5 6 /** 7 * 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。 8 * @return boolean 9 */ 10 public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { 11 StringBuffer sb = new StringBuffer(); 12 Set es = packageParams.entrySet(); 13 Iterator it = es.iterator(); 14 while(it.hasNext()) { 15 Map.Entry entry = (Map.Entry)it.next(); 16 String k = (String)entry.getKey(); 17 String v = (String)entry.getValue(); 18 if(!"sign".equals(k) && null != v && !"".equals(v)) { 19 sb.append(k + "=" + v + "&"); 20 } 21 } 22 23 sb.append("key=" + API_KEY); 24 25 //算出摘要 26 String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase(); 27 String tenpaySign = ((String)packageParams.get("sign")).toLowerCase(); 28 29 //System.out.println(tenpaySign + " " + mysign); 30 return tenpaySign.equals(mysign); 31 } 32 33 /** 34 * @Description:sign签名 35 * @param characterEncoding 编码格式 36 * @param packageParams 请求参数 37 * @param API_KEY 38 * @return 39 */ 40 public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { 41 StringBuffer sb = new StringBuffer(); 42 Set es = packageParams.entrySet(); 43 Iterator it = es.iterator(); 44 while (it.hasNext()) { 45 Map.Entry entry = (Map.Entry) it.next(); 46 String k = (String) entry.getKey(); 47 String v = (String) entry.getValue(); 48 if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { 49 sb.append(k + "=" + v + "&"); 50 } 51 } 52 sb.append("key=" + API_KEY); 53 String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase(); 54 return sign; 55 } 56 57 /** 58 * @Description:将请求参数转换为xml格式的string 59 * @param parameters 请求参数 60 * @return 61 */ 62 public static String getRequestXml(SortedMap<Object, Object> parameters) { 63 StringBuffer sb = new StringBuffer(); 64 sb.append("<xml>"); 65 Set es = parameters.entrySet(); 66 Iterator it = es.iterator(); 67 while (it.hasNext()) { 68 Map.Entry entry = (Map.Entry) it.next(); 69 String k = (String) entry.getKey(); 70 String v = (String) entry.getValue(); 71 if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) { 72 sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">"); 73 } else { 74 sb.append("<" + k + ">" + v + "</" + k + ">"); 75 } 76 } 77 sb.append("</xml>"); 78 return sb.toString(); 79 } 80 81 /** 82 * 取出一个指定长度大小的随机正整数 83 * @param length int 设定所取出随机数的长度。length小于11 84 * @return int 返回生成的随机数。 85 */ 86 public static int buildRandom(int length) { 87 int num = 1; 88 double random = Math.random(); 89 if (random < 0.1) { 90 random = random + 0.1; 91 } 92 for (int i = 0; i < length; i++) { 93 num = num * 10; 94 } 95 return (int) ((random * num)); 96 } 97 98 /** 99 * 获取当前时间 yyyyMMddHHmmss 100 * @return String 101 */ 102 public static String getCurrTime() { 103 Date now = new Date(); 104 SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss"); 105 String s = outFormat.format(now); 106 return s; 107 } 108 }
1 /** 2 * 发送支付请求 3 */ 4 public class HttpUtil { 5 private final static int CONNECT_TIMEOUT = 5000; // in milliseconds 6 private final static String DEFAULT_ENCODING = "UTF-8"; 7 8 public static String postData(String urlStr, String data){ 9 return postData(urlStr, data, null); 10 } 11 12 public static String postData(String urlStr, String data, String contentType){ 13 BufferedReader reader = null; 14 try { 15 URL url = new URL(urlStr); 16 URLConnection conn = url.openConnection(); 17 conn.setDoOutput(true); 18 conn.setConnectTimeout(CONNECT_TIMEOUT); 19 conn.setReadTimeout(CONNECT_TIMEOUT); 20 if(contentType != null) 21 conn.setRequestProperty("content-type", contentType); 22 OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING); 23 if(data == null) 24 data = ""; 25 writer.write(data); 26 writer.flush(); 27 writer.close(); 28 29 reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING)); 30 StringBuilder sb = new StringBuilder(); 31 String line = null; 32 while ((line = reader.readLine()) != null) { 33 sb.append(line); 34 sb.append("\r\n"); 35 } 36 return sb.toString(); 37 } catch (IOException e) { 38 System.out.println("Error connecting to " + urlStr + ": " + e.getMessage()); 39 } finally { 40 try { 41 if (reader != null) 42 reader.close(); 43 } catch (IOException e) { 44 } 45 } 46 return null; 47 } 48 }
1 /** 2 * 微信返回结果解析 3 */ 4 public class XMLUtil { 5 /** 6 * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 7 * 8 * @param strxml 9 * @return 10 * @throws JDOMException 11 * @throws IOException 12 */ 13 public static Map<String, String> doXMLParse(String strxml) throws JDOMException, IOException { 14 strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\""); 15 16 if (null == strxml || "".equals(strxml)) { 17 return null; 18 } 19 20 Map<String, String> m = new HashMap<>(); 21 22 InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8")); 23 SAXBuilder builder = new SAXBuilder(); 24 Document doc = builder.build(in); 25 Element root = doc.getRootElement(); 26 List list = root.getChildren(); 27 Iterator it = list.iterator(); 28 while (it.hasNext()) { 29 Element e = (Element) it.next(); 30 String k = e.getName(); 31 String v = ""; 32 List children = e.getChildren(); 33 if (children.isEmpty()) { 34 v = e.getTextNormalize(); 35 } else { 36 v = XMLUtil.getChildrenText(children); 37 } 38 m.put(k, v); 39 } 40 // 关闭流 41 in.close(); 42 return m; 43 } 44 45 /** 46 * 获取子结点的xml 47 * @param children 48 * @return String 49 */ 50 public static String getChildrenText(List children) { 51 StringBuffer sb = new StringBuffer(); 52 if (!children.isEmpty()) { 53 Iterator it = children.iterator(); 54 while (it.hasNext()) { 55 Element e = (Element) it.next(); 56 String name = e.getName(); 57 String value = e.getTextNormalize(); 58 List list = e.getChildren(); 59 sb.append("<" + name + ">"); 60 if (!list.isEmpty()) { 61 sb.append(XMLUtil.getChildrenText(list)); 62 } 63 sb.append(value); 64 sb.append("</" + name + ">"); 65 } 66 } 67 return sb.toString(); 68 } 69 }
2、微信支付
①、页面请求
1 $.ajax({ 2 url:"weChatPay", 3 type:"post", 4 data: { 5 "用于生成订单的商品信息" 6 }, 7 success:function(res){ 8 var data = res.data; 9 if(data.msg != null && data.msg != ""){ 10 $.toast(data.msg, "cancel"); 11 }else { 12 WeixinJSBridge.invoke( 13 'getBrandWCPayRequest', { 14 "appId":data.appId, //公众号名称,由商户传入 15 "timeStamp":data.timeStamp, //时间戳,自1970年以来的秒数 16 "nonceStr":data.nonceStr, //随机串 17 "package":data.package, 18 "signType":"MD5", //微信签名方式: 19 "paySign":data.paySign //微信签名 20 }, 21 function(re){ 22 if(re.err_msg == "get_brand_wcpay_request:fail" ) 23 { 24 // 使用以上方式判断前端返回,微信团队郑重提示: 25 //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。 26 $.toast("支付失败,请稍后再试!", "cancel"); 27 } 28 else if(re.err_msg == "get_brand_wcpay_request:cancel") 29 { 30 $.toast("已取消支付!", "cancel"); 31 } 32 else if(re.err_msg == "get_brand_wcpay_request:ok") 33 { 34 $.toast("支付成功,请手动刷新订单查看!"); 35 } 36 }); 37 } 38 }, 39 error:function(e){ 40 $.toast("系统错误,支付失败!", "cancel"); 41 } 42 });
②、controller层
1 /** 2 * 微信公众号支付 3 * @param session 4 * @return 5 */ 6 @PostMapping("weChatPay") 7 public Response weChatPay(Object 订单需要信息,HttpSession session){ 8 //获取微信用户的openId---这个是用户访问页面的时候已经从微信获取保存在session里面了,可以通过微信授权的时候取到 9 String openId = session.getAttribute("openId").toString(); 10 //生成待支付订单 11 String orderCode = "订单唯一值"; 12 。。。此处省略了插入数据库模块 13 Map<String,Object> map = this.weChatService.weChatPay(openId,orderCode); 14 return new Response().success().data(map); 15 }
③、service层
1 public Map<String, Object> weChatPay(String openId,String orderCode) { 2 Map<String,Object> wechatmap = new HashMap<>(); 3 String msg= ""; 4 try{ 5 //通过订单取到支付金额即商品名称 6 。。。通过订单编号取订单信息 7 //订单金额 8 BigDecimal ordersAmount = null; 9 //商品名称 10 String body = ""; 11 //微信支付金额单位统一转换成分 12 BigDecimal fen = ordersAmount.multiply(new BigDecimal(100)); 13 fen = fen.setScale(0, BigDecimal.ROUND_HALF_UP); 14 String amount = fen.toString(); 15 16 //微信支付配置 17 String appid = PayConfigUtil.APP_ID; // appid 18 String mch_id = PayConfigUtil.MCH_ID; // 商业号 19 String key = PayConfigUtil.API_KEY; // key 20 21 String currTime = PayCommonUtil.getCurrTime(); 22 String strTime = currTime.substring(8, currTime.length()); 23 String strRandom = PayCommonUtil.buildRandom(4) + ""; 24 String nonce_str = strTime + strRandom; 25 26 String spbill_create_ip = PayConfigUtil.CREATE_IP;// 获取发起电脑 ip 27 String notify_url = PayConfigUtil.NOTIFY_URL;// 回调接口 28 String trade_type = "JSAPI";//公众号支付方式 29 30 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); 31 packageParams.put("appid", appid); 32 packageParams.put("openid",openId);//用户标识 33 packageParams.put("mch_id", mch_id); 34 packageParams.put("nonce_str", nonce_str); 35 packageParams.put("body", body); 36 packageParams.put("out_trade_no", orderCode); 37 packageParams.put("total_fee", order_price); 38 packageParams.put("spbill_create_ip", spbill_create_ip); 39 packageParams.put("notify_url", notify_url); 40 packageParams.put("trade_type", trade_type); 41 //附加类型-这里可以附加自己需要的特殊属性 42 packageParams.put("attach", "公众号一支付"); 43 44 //计算签名 45 String sign = PayCommonUtil.createSign("UTF-8", packageParams, key); 46 packageParams.put("sign", sign); 47 //把支付参数转换xml格式 48 String requestXML = PayCommonUtil.getRequestXml(packageParams); 49 //发起微信支付请求 50 String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML); 51 52 //解析返回结果 53 Map<String, String> map = XMLUtil.doXMLParse(resXml); 54 55 if(map.get("return_code").equals("FAIL")){ 56 msg = "下单错误"; 57 wechatmap.put("msg",msg); 58 wechatmap.put("error_code", map.get("return_msg")); 59 return wechatmap; 60 }else if(map.get("return_code").equals("SUCCESS")){ 61 if(map.get("result_code").equals("FAIL") 62 || (map.get("err_code") != null && !map.get("err_code").equals("")) 63 || (map.get("err_code_des") != null && !map.get("err_code_des").equals(""))){ 64 msg = "其他的错误参数值"; 65 wechatmap.put("msg",msg); 66 wechatmap.put("error_code", map.get("err_code_des")); 67 return wechatmap; 68 } 69 } 70 71 //预支付交易会话标识---这个值很关键 72 String prepay_id = (String) map.get("prepay_id"); 73 74 SortedMap<Object, Object> mapp = new TreeMap<Object, Object>(); 75 76 currTime = PayCommonUtil.getCurrTime(); 77 strTime = currTime.substring(8, currTime.length()); 78 strRandom = PayCommonUtil.buildRandom(4) + ""; 79 nonce_str = strTime + strRandom; 80 81 mapp.put("appId", appid); 82 mapp.put("timeStamp", System.currentTimeMillis());//重新计算时间戳 83 mapp.put("nonceStr", nonce_str);//重新计算随机字符串,长度要求在32位以内。 84 85 if(!prepay_id.equals("")) { 86 mapp.put("package", "prepay_id="+prepay_id); 87 } 88 89 mapp.put("signType", "MD5"); 90 wechatmap = createSign("UTF-8", mapp, key); 91 }catch (IOException e){ 92 msg = "系统异常,暂时无法使用微信支付,请联系客服!!"; 93 } 94 wechatmap.put("msg",msg); 95 return wechatmap; 96 }
④、util(公众号用户授权)
1 /** 2 * 微信授权重定向后进入的方法获取用户在本公众号的 唯一标示 3 * 能否授权成功并取到用户的openId是公众号各类操作成功的前提 4 */ 5 public static String[] obtainUnionid(HttpServletRequest request) 6 { 7 String CODE = request.getParameter("code"); 8 9 String[] openid = {"",""}; 10 11 if(CODE == null || CODE.equals("")) 12 { 13 openid[0] = "未授权!"; 14 } 15 else if(CODE.equals("10009"))//操作频繁 16 { 17 openid[0] = "操作频繁!"; 18 } 19 else if(CODE.equals("10004"))//此公众号被封禁 20 { 21 openid[0] = "此公众号被封禁!"; 22 } 23 else if(CODE.equals("10006"))//必须关注此测试号 24 { 25 openid[0] = "必须先关注!"; 26 } 27 else if(CODE.equals("10015"))//公众号未授权第三方平台,请检查授权状态 28 { 29 openid[0] = "此公众不支持!"; 30 } 31 else if (CODE.equals("10003") || CODE.equals("10005") || CODE.equals("10007") || CODE.equals("10008") 32 || CODE.equals("10010") || CODE.equals("10011") || CODE.equals("10012") || CODE.equals("10013") 33 || CODE.equals("10014") || CODE.equals("10016")) 34 { 35 openid[0] = "系统错误,请稍后再试!"; 36 } 37 else 38 { 39 String APPID = WeChatConfig.getInstance().getAppId(); 40 String SECRET = WeChatConfig.getInstance().getAppSecret(); 41 42 String URL = WeChatConsts.URL_OAUTH2_GET_ACCESSTOKEN.replace("APPID", APPID).replace("SECRET", SECRET).replace("CODE", CODE); 43 44 JSONObject jsonStr = WeChatRealization.httpRequest(URL,"GET",null); 45 46 if(jsonStr.get("errcode") != null && !jsonStr.get("errcode").equals("")) 47 { 48 //失败 49 openid[0] = "此公众不支持!!!"; 50 } 51 else 52 { 53 openid[1] = jsonStr.get("openid").toString();//只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 54 } 55 } 56 return openid; 57 }
3、微信回调
①、controller层
1 /** 2 * 微信支付回调 3 * @param request 4 * @param response 5 */ 6 @RequestMapping(value = "payBackWx", method = {RequestMethod.GET,RequestMethod.POST}) 7 public void payBackWx(HttpServletRequest request, HttpServletResponse response){ 8 weChatService.payBackWx(request,response); 9 }
②、service层
1 public void payBackWx(HttpServletRequest request, HttpServletResponse response) { 2 response.setContentType("text/xml"); 3 InputStream inputStream;// 读取参数 4 StringBuffer sb = new StringBuffer(); 5 try{ 6 inputStream = request.getInputStream(); 7 String s; 8 BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); 9 while ((s = in.readLine()) != null){ 10 sb.append(s); 11 } 12 in.close(); 13 inputStream.close(); 14 // 解析xml成map 15 Map<String, String> m = new HashMap<String, String>(); 16 m = XMLUtil.doXMLParse(sb.toString()); 17 18 // 过滤空 设置 TreeMap 19 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); 20 Iterator it = m.keySet().iterator(); 21 while (it.hasNext()){ 22 String parameter = (String) it.next(); 23 String parameterValue = m.get(parameter); 24 String v = ""; 25 if (null != parameterValue){ 26 v = parameterValue.trim(); 27 } 28 packageParams.put(parameter, v); 29 } 30 31 String key = PayConfigUtil.API_KEY; // key 32 String resXml = ""; 33 34 // 判断签名是否正确 35 if (PayCommonUtil.isTenpaySign("UTF-8", packageParams, key)){ 36 // 处理业务开始 37 if ("SUCCESS".equals((String) packageParams.get("result_code"))){ 38 // 这里是支付成功 39 ////////// 自己的业务逻辑//////////////// 40 String out_trade_no = packageParams.get("out_trade_no").toString();//商户订单号 41 String mch_id = packageParams.get("mch_id").toString();//商户号 42 String openid = packageParams.get("openid").toString();//用户标识 43 String total_fee = packageParams.get("total_fee").toString();//金额 为 分 44 String attach = packageParams.get("attach").toString();//自己的附加属性 45 // 分 换成元 46 Double total_fee2 = Double.valueOf(total_fee) / 100; 47 //微信支付订单号 48 String transaction_id = packageParams.get("transaction_id").toString(); 49 。。。修改订单状态为支付成功 50 // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. 51 resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "" + "</xml> "; 52 }else{ 53 resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" 54 + "<return_msg><![CDATA[失败]]></return_msg>" + "</xml> "; 55 } 56 response.getWriter().write(resXml); 57 response.getWriter().flush(); 58 }else{ 59 //log通知签名验证失败 60 } 61 }catch (IOException e){ 62 //log获取微信充值返回参数错误 63 }catch(JDOMException e){ 64 //log微信充值返回数据xml转换为map错误 65 } 66 }
4、常见问题
①、此公众号并没有这些scope的权限,错误码:10005
建议检查一下公众号的功能。比如是不是在订阅号/未认证的公众号里面尝试调用认证服务号的功能。
微信支付认证过期或者APPID填写错误。
请使用snsapi_userinfo的授权登录方式即可解决。
②、商家暂时没有此类交易权限,请联系商家客服
请检查你的下单接口是否指定了支付用户的身份,需单独开通指定身份支付权限方可使用。
请确认你使用的商户号是否有jsapi支付的权限,可登录商户平台-产品中心查看。
③、当前页面的URL未注册:http://www.weixin.qq.com/pay.do
请检查下单接口中使用的商户号是否在商户平台(http://pay.weixin.qq.com)配置了对应的支付目录。
④、redirect_url域名与后台配置不一致,错误码:10003
本错误是公众号获取openid接口报的错误,接口文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
检查下单接口传的appid与获取openid接口的appid是否同一个(需一致)。
检查appid对应的公众号后台(mp.weixin.qq.com),是否配置的授权域名和获取openid的域名一致。授权域名配置路径:公众平台--设置--公众号设置--功能设置--网页授权域名。
⑤、该商户暂不支持通过外部拉起微信完成支付
Jsapi支付只能从微信浏览器内发起支付请求。
⑥、使用公众号时需要在公众号内设置IP白名单,需要把自己的公网IP/服务器IP加到白名单内。
⑦、安全域名、业务域名
都有每月设置次数限制,测试的时候不要经常去换域名信息,要不然上线的时候就没机会换域名了。(设置域名时注意,公众号内有个用户验证的MP_verify_xxxxxxxxx.txt文件,微信提示说要放在根目录下面,其实只要把文件内容返回出来,访问名称设置成文件名称加后缀就能通过验证了)。
三、微信扫码支付
微信扫码支付需要的参数有:APP_ID(微信公众号开发者ID)、MCH_ID(商户ID)、API_KEY(商户密钥)。
微信扫码支付一般应用的场景是PC端电脑支付。微信扫码支付可分为两种模式,根据支付场景选择相应模式。一般情况下的PC端扫码支付选择的是模式二,需要注意的是模式二无回调函数。
【模式一】商户后台系统根据微信支付规则链接生成二维码,链接中带固定参数productid(可定义为产品标识或订单号)。用户扫码后,微信支付系统将productid和用户唯一标识(openid)回调商户后台系统(需要设置支付回调URL),商户后台系统根据productid生成支付交易,最后微信支付系统发起用户支付流程。
【模式二】商户后台系统调用微信支付【统一下单API】生成预付交易,将接口返回的链接生成二维码,用户扫码后输入密码完成支付交易。注意:该模式的预付单有效期为2小时,过期后无法支付。
注意:需要回调时需要在商户里面配置扫码回调地址(可以在前面的资料准备-信息位置里面看图)
1、工具类
需要的工具类与上面微信公众号支付的一致
2、微信支付
这里我们使用的是微信扫码支付的【模式二】,使用链接生成二维码方式
①、页面请求
1 //引用二维码生成JS---直接网上下载一个就行了 2 <script src="qrcode.js"></script> 3 4 $.ajax({ 5 url : 'weChatPayment', 6 type : 'post', 7 async: true, 8 data : formData, 9 success : function(data) { 10 if(data!=null && data!="" && data.length > 0){ 11 if(data[0] != ""){ 12 if(data[0] == 1){ 13 //页面中间显示图片 14 $("#qrcode").html(""); 15 var qrcode = new QRCode('qrcode'); 16 qrcode.makeCode(data[1]); 17 }else{ 18 layer.msg(data[0], {icon: 2}); 19 } 20 }else{ 21 layer.msg("错误!!!", {icon: 2}); 22 } 23 }else{ 24 layer.msg("错误!!!", {icon: 2}); 25 } 26 }, 27 error:function(e){ 28 layer.msg("网络错误!!!", {icon: 2}); 29 } 30 });
②、controller层
1 /** 2 * 微信充值前 3 * @param 订单需要信息 4 * @return 5 */ 6 @PostMapping("weChatPayment") 7 public Object weChatPayment(Object 订单需要信息){ 8 //生成待支付订单 9 String orderCode = "订单唯一值"; 10 。。。此处省略了插入数据库模块 11 return paymentService.weChatPayment(orderCode); 12 }
③、service层
1 @Transactional 2 public String[] weChatPayment(String orderCode) { 3 String[] str = new String[2]; 4 try{ 5 //通过订单取到支付金额即商品名称 6 。。。通过订单编号取订单信息 7 //订单金额 8 BigDecimal ordersAmount = null; 9 BigDecimal fen = new BigDecimal(ordersAmount).multiply(new BigDecimal(100)); 10 fen = fen.setScale(0, BigDecimal.ROUND_HALF_UP); 11 String amount = fen.toString(); 12 13 //微信支付配置 14 String appid = PayConfigUtil.APP_ID; // appid 15 String mch_id = PayConfigUtil.MCH_ID; // 商业号 16 String key = PayConfigUtil.API_KEY; // key 17 18 String currTime = PayCommonUtil.getCurrTime(); 19 String strTime = currTime.substring(8, currTime.length()); 20 String strRandom = PayCommonUtil.buildRandom(4) + ""; 21 String nonce_str = strTime + strRandom; 22 23 String spbill_create_ip = PayConfigUtil.CREATE_IP;// 获取发起电脑 ip 24 String notify_url = PayConfigUtil.NOTIFY_URL;// 回调接口 25 String trade_type = "NATIVE";//扫码支付方式 26 27 String order_price = amount; // 价格 注意:价格的单位是分 28 String body = "账户充值"; // 商品名称 29 30 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); 31 packageParams.put("appid", appid); 32 packageParams.put("mch_id", mch_id); 33 packageParams.put("nonce_str", nonce_str); 34 packageParams.put("body", body); 35 packageParams.put("out_trade_no", orderCode); 36 packageParams.put("total_fee", order_price); 37 packageParams.put("spbill_create_ip", spbill_create_ip); 38 packageParams.put("notify_url", notify_url); 39 packageParams.put("trade_type", trade_type); 40 41 //计算签名 42 String sign = PayCommonUtil.createSign("UTF-8", packageParams, key); 43 packageParams.put("sign", sign); 44 //把支付参数转换xml格式 45 String requestXML = PayCommonUtil.getRequestXml(packageParams); 46 //发起微信支付请求 47 String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML); 48 Map<String, String> map = XMLUtil.doXMLParse(resXml); 49 50 //获取得到充值url-前台生成二维码使用 51 String urlCode = (String) map.get("code_url"); 52 if(urlCode != null && !urlCode.equals("")){ 53 str[0] = "1"; 54 str[1] = urlCode; 55 return str; 56 }else{ 57 //系统异常,暂时无法使用微信支付,请联系客服 58 str[0] = "系统异常,暂时无法使用微信支付,请联系客服"; 59 } 60 }catch (IOException e){ 61 str[0] = "系统异常,暂时无法使用微信支付,请联系客服!!"; 62 } 63 return str; 64 }
3、微信回调
回调和解析方式都与微信公众号支付回调一致
四、微信H5支付
微信H5支付需要的参数有:APP_ID(微信公众号开发者ID)、MCH_ID(商户ID)、API_KEY(商户密钥)。
微信H5支付是微信官方2017年上半年刚刚对外开放的支付模式,它主要应用于在手机网站在移动浏览器(非微信环境)调用微信支付的场景。底层的技术以及支付链接本质上是财付通。
微信H5支付的流程比较简单,就是拼接请求的xml数据,进行统一下单,获取到支付的mweb_url,然后请求这个url网址就行。请求使用curl函数,使用的时候需要注意设置header参数。
注意:微信H5支付需要在微信支付商户平台单独申请开通,否则无法使用。(可以在前面的资料准备-信息位置里面看图)
1、工具类
需要的工具类与上面微信公众号支付的一致
2、微信支付
①、页面请求
1 $.ajax({ 2 url: 'pay', 3 type: 'POST', 4 dataType: "json", 5 data:{}, 6 success: function (res) { 7 $.hideLoading(); 8 var data = res.data; 9 if(data!=null && data!="" && data.length > 0){ 10 if(data[0] != ""){ 11 if(data[0] == 1){ 12 $("#submit_z").text("正在支付"); 13 //原页面显示一个需手动确认的支付提示信息 14 layer.open({ 15 type: 1 16 ,title: false //不显示标题栏 17 ,closeBtn: false 18 ,area:['240px','150px'] 19 ,shade: 0.3 20 ,offset: 'auto' 21 ,id: 'LAY_layuipro' //设定一个id,防止重复弹出 22 ,content: '<div style="width: 100%;text-align: center;font-size: 14px;border-radius: 20px;">\n' + 23 ' <div style="width: 100%;height: 59px;line-height: 59px;border-bottom: 1px #ccc solid;">\n' + 24 ' 请确认微信支付是否已完成\n' + 25 ' </div>\n' + 26 ' <div onclick="pay_true();" style="width: 100%;font-weight: 500;border-bottom: 1px #ccc solid;line-height: 44px;height: 44px;color: red;">\n' + 27 ' 已完成支付\n' + 28 ' </div>\n' + 29 ' <div onclick="pay_false();" style="color: #9c9c9c;width: 100%;line-height: 45px;height: 45px;">\n' + 30 ' 支付遇到问题,重新支付\n' + 31 ' </div>\n' + 32 '</div>' 33 }); 34 }else{ 35 layer.msg(data[0], {icon: 2}); 36 } 37 } 38 else{ 39 layer.msg("错误!!!", {icon: 2}); 40 } 41 } 42 else{ 43 layer.msg("错误!!!", {icon: 2}); 44 } 45 }, 46 error:function(e){ 47 layer.msg("网络错误!!!", {icon: 2}); 48 } 49 }); 50 51 //跳转到订单详情 52 function pay_true() { 53 //跳转到订单页 54 window.location.href = [[${url}]]; 55 } 56 //回到支付前状态 57 function pay_false() { 58 layer.closeAll(); 59 $("#submit_z").text("提交订单"); 60 }
②、controller层
1 @PostMapping("pay") 2 public Response pay(Object obj,HttpServletRequest request) { 3 String request_url = request.getRequestURL().toString().split("indent/pay")[0]; 4 // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 5 // 一定要取到真实IP,不然后面支付会报错 6 String ip = null; 7 try { 8 ip = request.getHeader("x-forwarded-for"); 9 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 10 ip = request.getHeader("Proxy-Client-IP"); 11 } 12 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 13 ip = request.getHeader("WL-Proxy-Client-IP"); 14 } 15 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 16 ip = request.getHeader("HTTP_CLIENT_IP"); 17 } 18 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 19 ip = request.getHeader("HTTP_X_FORWARDED_FOR"); 20 } 21 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 22 ip = request.getRemoteAddr(); 23 if (ip.equals("127.0.0.1")) { 24 // 根据网卡取本机配置的IP 25 InetAddress inet = null; 26 try { 27 inet = InetAddress.getLocalHost(); 28 } catch (UnknownHostException e) { 29 //LOGGER_ERROR.error("抛出异常 # ",e); 30 } 31 ip = inet.getHostAddress(); 32 } 33 } 34 35 // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 36 if (ip != null && ip.length() > 15) { // "***.***.***.***".length() = 15 37 if (ip.indexOf(",") > 0) { 38 ip = ip.substring(0, ip.indexOf(",")); 39 } 40 } 41 } catch (Exception ex) { 42 // LOGGER_ERROR.error("NetworkUtil # getIpAddress # 抛出异常 # ", ex); 43 } 44 //生成待支付订单 45 String orderCode = "订单唯一值"; 46 。。。此处省略了插入数据库模块 47 return new Response().success().data(this.payService.pay(orderCode,request_url,ip)); 48 }
③、service层
1 @Transactional 2 public String[] pay(String orderCode, String url,String ip) { 3 String[] str = new String[2]; 4 String message = ""; 5 try { 6 7 //通过订单取到支付金额即商品名称 8 。。。通过订单编号取订单信息 9 //订单金额 10 BigDecimal ordersAmount = null; 11 // 商品名称 12 String body = ""; 13 14 //获取支付配置 15 String appid = PayConfigUtil.APP_ID;// appid 16 String mch_id = PayConfigUtil.MCH_ID;// 商业号 17 String key = PayConfigUtil.API_KEY;// 商户API-key 18 19 String currTime = PayCommonUtil.getCurrTime(); 20 String strTime = currTime.substring(8, currTime.length()); 21 String strRandom = PayCommonUtil.buildRandom(4) + ""; 22 String nonce_str = strTime + strRandom; 23 24 // 价格 注意:价格的单位是分 25 String order_price = new BigDecimal(String.valueOf(ordersAmount)).multiply(new BigDecimal("100")).setScale(0,BigDecimal.ROUND_HALF_UP).toString(); 26 27 // 获取发起电脑 ip----这里的IP限制跟别的支付不一样,这里需要真实IP 28 String spbill_create_ip = ip; 29 30 // 回调接口 31 String notify_url = PayConfigUtil.NOTIFY_URL; 32 33 //JSAPI NATIVE 代表公众号支付 MWEB 代表H5 34 String trade_type = "MWEB"; 35 36 SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); 37 38 packageParams.put("appid", appid);//公众账号ID 39 40 packageParams.put("mch_id", mch_id);//商户号 41 42 //packageParams.put("device_info", "WEB"); 43 //自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" 44 45 packageParams.put("nonce_str", nonce_str); 46 //随机字符串,长度要求在32位以内。 47 packageParams.put("body", body); 48 //商品简单描述 49 packageParams.put("out_trade_no", orderCode); 50 //商户订单号 51 packageParams.put("total_fee", order_price); 52 //标价金额 单位为分 53 packageParams.put("spbill_create_ip", spbill_create_ip); 54 //APP和网页支付提交用户端ip 55 packageParams.put("notify_url", notify_url); 56 //异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 57 packageParams.put("trade_type", trade_type); 58 packageParams.put("scene_info","'h5_info':{'type':'Wap','wap_url':'"+url+"/shop','wap_name': "+body+""); 59 60 String sign = PayCommonUtil.createSign("UTF-8", packageParams, key); 61 packageParams.put("sign", sign); 62 //通过签名算法计算得出的签名值 63 64 String requestXML = PayCommonUtil.getRequestXml(packageParams); 65 66 String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML); 67 68 69 Map<String, String> map = XMLUtil.doXMLParse(resXml); 70 71 String urlCode = (String) map.get("code_url");//获取得到充值url 72 73 // 确认支付过后跳的地址,需要经过urlencode处理 74 String mweb_url = map.get("mweb_url") + "&redirect_url=" + PayConfigUtil.NOTIFY_URL; 75 76 if(mweb_url != null && !mweb_url.equals("")){ 77 str[0] = "1"; 78 str[1] = mweb_url; 79 return str; 80 }else{ 81 //系统异常,暂时无法使用微信支付,请联系客服 82 str[0] = "系统异常,暂时无法使用微信支付,请联系客服"; 83 } 84 }catch (Exception e){ 85 str[0] = "系统异常,暂时无法使用微信支付,请联系客服!!"; 86 } 87 return str; 88 }
3、微信回调
回调和解析方式都与微信公众号支付回调一致
4、常见问题
①、网络环境未能通过安全验证,请稍后再试
商户侧统一下单传的终端IP(spbill_create_ip)与用户实际调起支付时微信侧检测到的终端IP不一致导致的,这个问题一般是商户在统一下单时没有传递正确的终端IP到spbill_create_ip导致,详细可参见客户端ip获取指引(本文已经提供了一个获取用户真实IP的方法)。
统一下单与调起支付时的网络有变动,如统一下单时是WIFI网络,下单成功后切换成4G网络再调起支付,这样可能会引发我们的正常拦截,请保持网络环境一致的情况下重新发起支付流程。
②、商家参数格式有误,请联系商家解决
当前调起H5支付的referer为空导致,一般是因为直接访问页面调起H5支付,请按正常流程进行页面跳转后发起支付,或自行抓包确认referer值是否为空。
如果是APP里调起H5支付,需要在webview中手动设置referer,如(Map extraHeaders = new HashMap();extraHeaders.put("Referer", "商户申请H5时提交的授权域名");//例如 http://www.baidu.com ))。
③、商家存在未配置的参数,请联系商家解决
当前调起H5支付的域名(微信侧从referer中获取)与申请H5支付时提交的授权域名不一致,如需添加或修改授权域名,请登陆商户号对应的商户平台--"产品中心"--"开发配置"自行配置。
如果设置了回跳地址redirect_url,请确认设置的回跳地址的域名与申请H5支付时提交的授权域名是否一致。
④、支付请求已失效,请重新发起支付
统一下单返回的MWEB_URL生成后,有效期为5分钟,如超时请重新生成MWEB_URL后再发起支付。
⑤、请在微信外打开订单,进行支付
H5支付不能直接在微信客户端内调起,请在外部浏览器调起
⑥、IOS:签名验证失败、安卓:系统繁忙,请稍后再试
请确认同一个MWEB_URL只被一个微信号调起,如果不同微信号调起请重新下单生成新的MWEB_URL。
如MWEB_URL有添加redirect_url,请确认参数拼接格式是否有误,是否有对redirect_url的值做urlencode,可对比以下例子格式:https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096&redirect_url=https%3A%2F%2Fwww.wechatpay.com.cn
五、微信APP支付、微信小程序支付
结合前面几种支付,都是套娃式的