说起来.微信支付真是一堆坑. 居然官网都没有java版本的完整代码. 就算是php版本的.还都有错误.且前后各种版本.各种文档一大堆....不停的误导开发人员.
花了一天半时间.总算实现了微信公众号支付.和pc端的微信扫码支付.其他不说了.直接给思路
本人做的是微信V3版本的微信支付.微信的官方文档中.提供的demo 只有一些工具类.这些类还是很有作用的.
https://mp.weixin.qq.com/paymch/readtemplate?t=mp/business/course3_tmpl&lang=zh_CN 可以在这个连接中找到相应的java类.
这里一定要注意.在官网填写的授权目录一定要写到三级目录.如:
我的回调地址是:http://111.111.111.111:1111/control/weixinPay_notify
那么,官网填写都授权目录就是:http://111.111.111.111:1111/control/
我试过.授权目录写到2级.是没用的.此处差评,第一个坑.
首先,定义各种微信支付所需要的参数
GzhConfig.java
public static String APPID = "XXXXXXXXXXXX"; //受理商ID,身份标识 public static String MCHID = "XXXXXXXXXXXXxx"; //商户支付密钥Key。审核通过后,在微信发送的邮件中查看 public static String KEY = "XXXXXXXXXXXXXXXXX"; //JSAPI接口中获取openid,审核后在公众平台开启开发模式后可查看 public static String APPSECRET = "xxxxxxxxxxxxxx"; //重定向地址 public static String REDIRECT_URL = "http://XXXXXXXXXXXXXXXXXXX/callWeiXinPay"; //异步回调地址 public static String NOTIFY_URL = "http://XXXXXXXXXXXXXXXXXXXXXX/weixinPay_notify"; //web回调地址 public static String WEB_NOTIFY_URL = "http://XXXXXXXXXXXXXXXXXXXXXXXXX/weixinPay_notify";
然后.就是正式的开始代码了:
1.使用Oauth2.0授权.进行页面跳转,获取code .(code关系着后面获取openid.)
https://open.weixin.qq.com/connect/oauth2/authorize?appid=123456789&redirect_uri=http://111.111.111.111:1111/control/orderPay&response_type=code&scope=snsapi_base&state=456123456#wechat_redirect
此处.appid 这个在微信官网可以获取. 重定向地址. 就是获取code 后.跳转指向你的地址.这里可以是你的订单结算页面.response_type=code和scope=snsapi_base 都是固定格式. state 是传入传出.这个参数用户自定义为任何都可以,比如说订单id. 然后会和code 一起传递到你的重定向地址,如我上面写的重定向地址就是 orderPay链接.
2.在重定向到页面(http://111.111.111.111:1111/control/orderPay)的时候中间执行java方法(如获取openid 如执行微信统一下单接口,获取预支付ID.).处理各种参数.下面贴具体代码做说明.
GzhService.java
String code = request.getParameter("code"); String state = request.getParameter("state"); Debug.log("code-======"+code+"===========state======"+state); String noncestr = Sha1Util.getNonceStr();//生成随机字符串 String timestamp = Sha1Util.getTimeStamp();//生成1970年到现在的秒数. //state 可以传递你的订单号.然后根据订单号 查询付款金额.我就不详细写了. String out_trade_no = state;//订单号 GenericValue orderHeader = delegator.findOne("OrderHeader", UtilMisc.toMap("orderId", out_trade_no),false); String total_fee = String.valueOf(orderHeader.getBigDecimal("grandTotal").doubleValue()*100); String order_price = total_fee.substring(0, total_fee.indexOf("."));//订单金额 //微信金额 以分为单位.这是第二坑.如果不注意.页面的报错.你基本看不出来.因为他提示系统升级,正在维护.扯淡..... String product_name=out_trade_no;//订单名称 //获取jsapi_ticket.此参数是为了生成 js api 加载时候的签名用.必须.jsapi_ticket只会存在7200秒.并且有时间限制,(好像一年还只能调用两万次.所以一定要缓存.)这是第三坑. //可以在java中模拟url请求.就能获取access_token 然后根据access_token 取得 jsapi_ticket,但一定要缓存起来..这个代码.我只提供获取.缓存你们自己处理. //SendGET方法我会在后面放出 String tokenParam = "grant_type=client_credential&appid="+GzhConfig.APPID+"&secret="+GzhConfig.APPSECRET; String tokenJsonStr = SendGET("https://api.weixin.qq.com/cgi-bin/token", tokenParam); Map tokenMap = JSON.parseObject(tokenJsonStr); //获取access_token String access_token = (String)tokenMap.get("access_token"); String ticketParam = "access_token="+access_token+"&type=jsapi"; String ticketJsonStr = SendGET("https://api.weixin.qq.com/cgi-bin/ticket/getticket", ticketParam); Map ticketMap = JSON.parseObject(ticketJsonStr); //获取jsapi_ticket String ticket = (String)ticketMap.get("ticket"); //下面就到了获取openid,这个代表用户id. //获取openID String openParam = "appid="+GzhConfig.APPID+"&secret="+GzhConfig.APPSECRET+"&code="+code+"&grant_type=authorization_code"; String openJsonStr = SendGET("https://api.weixin.qq.com/sns/oauth2/access_token", openParam); Map openMap = JSON.parseObject(openJsonStr); String openid = (String) openMap.get("openid"); RequestHandler reqHandler = new RequestHandler(request, response); //初始化 RequestHandler 类可以在微信的文档中找到.还有相关工具类 reqHandler.init(); reqHandler.init(GzhConfig.APPID, GzhConfig.APPSECRET, GzhConfig.KEY, ""); //执行统一下单接口 获得预支付id reqHandler.setParameter("appid",GzhConfig.APPID); reqHandler.setParameter("mch_id", GzhConfig.MCHID); //商户号 reqHandler.setParameter("nonce_str", noncestr); //随机字符串 reqHandler.setParameter("body", product_name); //商品描述(必填.如果不填.也会提示系统升级.正在维护我艹.) reqHandler.setParameter("out_trade_no", out_trade_no); //商家订单号 reqHandler.setParameter("total_fee", order_price); //商品金额,以分为单位 reqHandler.setParameter("spbill_create_ip",request.getRemoteAddr()); //用户的公网ip IpAddressUtil.getIpAddr(request) //下面的notify_url是用户支付成功后为微信调用的action 异步回调. reqHandler.setParameter("notify_url", GzhConfig.NOTIFY_URL); reqHandler.setParameter("trade_type", "JSAPI"); //------------需要进行用户授权获取用户openid------------- reqHandler.setParameter("openid", openid); //这个必填. //这里只是在组装数据.还没到执行到统一下单接口.因为统一下单接口的数据传递格式是xml的.所以才需要组装. String requestUrl = reqHandler.getRequestURL(); requestUrl 例子: /* <xml><appid>wx2421b1c4370ec43b</appid><attach>支付测试</attach><body>JSAPI支付测试</body><mch_id>10000100</mch_id><nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str><notify_url>http://wxpay.weixin.qq.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> */ Debug.log("requestUrl==================="+requestUrl); //统一下单接口提交 xml格式 URL orderUrl = new URL("https://api.mch.weixin.qq.com/pay/unifiedorder"); HttpURLConnection conn = (HttpURLConnection) orderUrl.openConnection(); conn.setConnectTimeout(30000); // 设置连接主机超时(单位:毫秒) conn.setReadTimeout(30000); // 设置从主机读取数据超时(单位:毫秒) conn.setDoOutput(true); // post请求参数要放在http正文内,顾设置成true,默认是false conn.setDoInput(true); // 设置是否从httpUrlConnection读入,默认情况下是true conn.setUseCaches(false); // Post 请求不能使用缓存 // 设定传送的内容类型是可序列化的java对象(如果不设此项,在传送序列化对象时,当WEB服务默认的不是这种类型时可能抛java.io.EOFException) conn.setRequestProperty("Content-Type","application/x-www-form-urlencoded"); conn.setRequestMethod("POST");// 设定请求的方法为"POST",默认是GET conn.setRequestProperty("Content-Length",requestUrl.length()+""); String encode = "utf-8"; OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(), encode); out.write(requestUrl.toString()); out.flush(); out.close(); String result = getOut(conn); Debug.log("result=========返回的xml============="+result); Map<String, String> orderMap = XMLUtil.doXMLParse(result); Debug.log("orderMap==========================="+orderMap); //得到的预支付id String prepay_id = orderMap.get("prepay_id"); SortedMap<String,String> params = new TreeMap<String,String>(); params.put("appId", GzhConfig.APPID); params.put("timeStamp",timestamp); params.put("nonceStr", noncestr); params.put("package", "prepay_id="+prepay_id); params.put("signType", "MD5"); //生成支付签名,这个签名 给 微信支付的调用使用 String paySign = reqHandler.createSign(params); request.setAttribute("paySign", paySign); request.setAttribute("appId", GzhConfig.APPID); request.setAttribute("timeStamp", timestamp); //时间戳 request.setAttribute("nonceStr", noncestr); //随机字符串 request.setAttribute("signType", "MD5"); //加密格式 request.setAttribute("out_trade_no", out_trade_no); //订单号 request.setAttribute("package", "prepay_id="+prepay_id);//预支付id ,就这样的格式. String url = "http://xxxxxxxxxx/control/wxPayment"; String signValue = "jsapi_ticket="+ticket+"&noncestr="+noncestr+"×tamp="+timestamp+"&url="+url; Debug.log("url====="+signValue); //这个签名.主要是给加载微信js使用.别和上面的搞混了. String signature = Sha1Util.getSha1((signValue)); request.setAttribute("signature", signature);
//此页面的一些其他方法
public static String getOut(HttpURLConnection conn) throws IOException{ if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { return null; } // 获取响应内容体 BufferedReader in = new BufferedReader(new InputStreamReader( conn.getInputStream(), "UTF-8")); String line = ""; StringBuffer strBuf = new StringBuffer(); while ((line = in.readLine()) != null) { strBuf.append(line).append("\n"); } in.close(); return strBuf.toString().trim(); }
public static String SendGET(String url,String param){ String result="";//访问返回结果 BufferedReader read=null;//读取访问结果 try { //创建url URL realurl=new URL(url+"?"+param); //打开连接 URLConnection connection=realurl.openConnection(); // 设置通用的请求属性 connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); //建立连接 connection.connect(); // 获取所有响应头字段 // Map<String, List<String>> map = connection.getHeaderFields(); // 遍历所有的响应头字段,获取到cookies等 // for (String key : map.keySet()) { // System.out.println(key + "--->" + map.get(key)); // } // 定义 BufferedReader输入流来读取URL的响应 read = new BufferedReader(new InputStreamReader( connection.getInputStream(),"UTF-8")); String line;//循环读取 while ((line = read.readLine()) != null) { result += line; } } catch (IOException e) { e.printStackTrace(); }finally{ if(read!=null){//关闭流 try { read.close(); } catch (IOException e) { e.printStackTrace(); } } } return result; }
其他相关类的方法:
/*
‘============================================================================
‘api说明:
‘createSHA1Sign创建签名SHA1
‘getSha1()Sha1签名
‘============================================================================
‘*/
public class Sha1Util { public static String getNonceStr() { Random random = new Random(); return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8"); } public static String getTimeStamp() { return String.valueOf(System.currentTimeMillis() / 1000); } //创建签名SHA1 public static String createSHA1Sign(SortedMap<String, String> signParams) throws Exception { StringBuffer sb = new StringBuffer(); Set es = signParams.entrySet(); Iterator it = es.iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); String k = (String) entry.getKey(); String v = (String) entry.getValue(); sb.append(k + "=" + v + "&"); //要采用URLENCODER的原始值! } String params = sb.substring(0, sb.lastIndexOf("&")); System.out.println("sha1 sb:" + params); return getSha1(params); } //Sha1签名 public static String getSha1(String str) { if (str == null || str.length() == 0) { return null; } char hexDigits[] = { ‘0‘, ‘1‘, ‘2‘, ‘3‘, ‘4‘, ‘5‘, ‘6‘, ‘7‘, ‘8‘, ‘9‘, ‘a‘, ‘b‘, ‘c‘, ‘d‘, ‘e‘, ‘f‘ }; try { MessageDigest mdTemp = MessageDigest.getInstance("SHA1"); mdTemp.update(str.getBytes("GBK")); byte[] md = mdTemp.digest(); int j = md.length; char buf[] = new char[j * 2]; int k = 0; for (int i = 0; i < j; i++) { byte byte0 = md[i]; buf[k++] = hexDigits[byte0 >>> 4 & 0xf]; buf[k++] = hexDigits[byte0 & 0xf]; } return new String(buf); } catch (Exception e) { return null; } } }
/**
* xml工具类
* @author miklchen
*
*/
public class XMLUtil { /** * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 * @param strxml * @return * @throws JDOMException * @throws IOException */ public static Map<String,String> doXMLParse(String strxml) throws JDOMException, IOException { if(null == strxml || "".equals(strxml)) { return null; } Map<String,String> m = new HashMap<String,String>(); InputStream in = HttpClientUtil.String2Inputstream(strxml); SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(in); Element root = doc.getRootElement(); List list = root.getChildren(); Iterator it = list.iterator(); while(it.hasNext()) { Element e = (Element) it.next(); String k = e.getName(); String v = ""; List children = e.getChildren(); if(children.isEmpty()) { v = e.getTextNormalize(); } else { v = XMLUtil.getChildrenText(children); } m.put(k, v); } //关闭流 in.close(); return m; } /** * 获取子结点的xml * @param children * @return String */ public static String getChildrenText(List children) { StringBuffer sb = new StringBuffer(); if(!children.isEmpty()) { Iterator it = children.iterator(); while(it.hasNext()) { Element e = (Element) it.next(); String name = e.getName(); String value = e.getTextNormalize(); List list = e.getChildren(); sb.append("<" + name + ">"); if(!list.isEmpty()) { sb.append(XMLUtil.getChildrenText(list)); } sb.append(value); sb.append("</" + name + ">"); } } return sb.toString(); } /** * 获取xml编码字符集 * @param strxml * @return * @throws IOException * @throws JDOMException */ public static String getXMLEncoding(String strxml) throws JDOMException, IOException { InputStream in = HttpClientUtil.String2Inputstream(strxml); SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(in); in.close(); return (String)doc.getProperty("encoding"); } }
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" }; }
下面才到了页面代码:jsp 页面
<script type="text/javascript" src="/xxxx/jquery-1.6.2.min.js"></script> <script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script> <script language="javascript"> //加载 wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: ‘${StringUtil.wrapString(requestAttributes.appId)!}‘, // 必填,公众号的唯一标识 timestamp: ${StringUtil.wrapString(requestAttributes.timeStamp)?default(0)!}, // 必填,生成签名的时间戳 nonceStr: ‘${StringUtil.wrapString(requestAttributes.nonceStr)!}‘, // 必填,生成签名的随机串 signature: ‘${StringUtil.wrapString(requestAttributes.signature)!}‘,// 必填,签名,见附录1 jsApiList: [ ‘checkJsApi‘, ‘chooseWXPay‘] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); wx.ready(function(){ //支付 wx.chooseWXPay({ timestamp: ${StringUtil.wrapString(requestAttributes.timeStamp)?default(0)!}, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 nonceStr: ‘${StringUtil.wrapString(requestAttributes.nonceStr)!}‘, // 支付签名随机串,不长于 32 位 package: ‘${StringUtil.wrapString(requestAttributes.package)!}‘, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***) signType: ‘${StringUtil.wrapString(requestAttributes.signType)!}‘, // 签名方式,默认为‘SHA1‘,使用新版支付需传入‘MD5‘ paySign: ‘${StringUtil.wrapString(requestAttributes.paySign)!}‘, // 支付签名 success: function (res) { // 支付成功后的回调函数 WeixinJSBridge.log(res.err_msg); //alert("支付接口:"+res.err_code + res.err_desc + res.err_msg); if(!res.err_msg){ //支付完后.跳转到成功页面. location.href="orderconfirm?orderId=${StringUtil.wrapString(requestAttributes.out_trade_no)!}"; } } }); // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 }); wx.error(function(res){ // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 }); wx.checkJsApi({ jsApiList: [‘chooseWXPay‘], // 需要检测的JS接口列表,所有JS接口列表见附录2, success: function(res) { //alert("检测接口:"+res.err_msg); } }); </script>
下面是后台异步回调代码:
/** * 异步返回 */ @SuppressWarnings("deprecation") public static String weixinNotify(HttpServletRequest request, HttpServletResponse response){ try { InputStream inStream = request.getInputStream(); ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = inStream.read(buffer)) != -1) { outSteam.write(buffer, 0, len); } outSteam.close(); inStream.close(); String resultStr = new String(outSteam.toByteArray(),"utf-8"); Map<String, String> resultMap = XMLUtil.doXMLParse(resultStr); String result_code = resultMap.get("result_code"); String is_subscribe = resultMap.get("is_subscribe"); String out_trade_no = resultMap.get("out_trade_no"); String transaction_id = resultMap.get("transaction_id"); String sign = resultMap.get("sign"); String time_end = resultMap.get("time_end"); String bank_type = resultMap.get("bank_type"); String return_code = resultMap.get("return_code"); //签名验证 GenericValue userLogin =delegator.findOne("UserLogin", UtilMisc.toMap("userLoginId","admin"),false); if(return_code.equals("SUCCESS")){ //此处就是你的逻辑代码 } request.setAttribute("out_trade_no", out_trade_no); //通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. response.getWriter().write(RequestHandler.setXML("SUCCESS", "")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (JDOMException e) { e.printStackTrace(); } return "success"; }
代码中,删除了一些和公司相关的代码.所以如果直接复制进去.肯定是要做大量修改的.见谅.