JAVA开发微信支付-公众号支付/微信浏览器支付(JSAPI)
写这篇文章的目的有2个,一是自己的项目刚开发完微信支付功能,趁热回个炉温习一下,二也是帮助像我这样对微信支付不熟悉,反复看了多天文档还是一知半解,原理都没摸清,更不要说实现了。本以为网上的微信开发教程会和“java的重写与重载”一样铺天盖地,可搜出来的结果,要么是PHP的教程(微信支付官网推荐就是PHP),要么星星点点就那么几篇,想对比的看看思路都成问题,官网下载的JAVA-SDK-DEMO也恕我技术低下,看的糊里糊涂。等自己开发完的那一刻,才豁然开朗,才知道走通完支付这条路的过程走了多少弯路,我是第一次接触支付,想必大部分能看这篇文章的兄弟也是被微信官方文档给绕的出不来才出此下策,内容有误请指正。好了这回真正的正题了:
步骤一:获取微信支付四大参数
首先要想支持微信支付,必须拥有两个账号:①微信公众已认证的服务号,并且需要开通微信支付该能(必须是企业才有资格申请,请你找你家产品去申请吧),②微信商户平台账号;这两个账号一个不能少。此处已默认你已有上两个账号。
此处是账号模板,请参考:
微信公众平台:账户:con*******om 登录密码 ******
公众APPID:wx15*********a8
APPSECEPT : c210***************892d7
微信商户平台:账户:149**********6742 登录密码:******
商户ID:14******42
API密钥:5d5************b35b
其中比较不好找的是商户的API密钥:在商户平台的账户中心下:需要用户自行下载证书及安装,(略)
至此我们需要的APPID(appid),APPSECEPT(appsecret),商户ID(mch_id),API密钥(paternerKey),四个重要参数已拿到,括号中是我们代码所用的变量名称请提前熟悉。
步骤二:平台配置
1.配置支付目录:商户平台:
配置此目录是代码中“微信支付”所在页面的地址,可以是目录不一定是全路径-如http://www.wangtao.com/order/-----此一级域名需要ICP备案。
点击添加
2.配置授权域名:微信公众平台:
支付过程需要获取用户openid,必须经过网页授权配置才可以,要不然获取不到openid。
点击设置,按说明设置
步骤三:开发流程:
微信支付原理(说白了就是调用官方文档的“统一下单”接口,之后将微信服务器返回的参数做个加工后,返回到前台(JSP页面),就OK了。咱们要做的就是想方设法的凑齐统一下单的所有参数“而已”,“而已”,“而已”,这个而已也就是最大的挑战)。所有参数解释请参考:官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1;
咱们只考虑必填参数,其他参数可以就看你的了。
先来看看所有参数:
其中的必填参数有:
1. appid APPID (已有)
2. mch_id 商户ID (已有)
3. nonce_str 随机字符串
4. sign 签名
5. body 所支付的名称
6. out_trade_no 咱们自己所提供的订单号,需要唯一
7. total_fee 支付金额
8. spbill_create_ip IP地址
9. notify_url 回调地址
10. trade_type 支付类型
11. openid 支付人的微信公众号对应的唯一标识
只要把这11个凑齐就齐活,现在咱们从第3个开始一个一个的获取;在这之前先从官网把公众号支付的sdk下载下来,如图
主要是用其中的WXPayUtil工具类中的一些方法。当然其他的类我看不懂,要是看懂了,就不至于这么费劲了。
好了开始咱们的取值之旅了:
1. appid APPID (已有)
2. mch_id 商户ID (已有)
3. nonce_str 随机字符串用WXPayUtil中的generateNonceStr()即可,就是生成UUID的方法;
4. sign 签名 用WXPayUtil中的generateSignature(finalMap<String, String> data, String key)方法,data是将除了sign外,其他10个参数放到map中,key是四大配置参数中的API秘钥(paternerKey)(这里不要着急管它,最后处理它);
5. body 所支付的名称
6. out_trade_no 自己后台生成的订单号,只要保证唯一就好:如“2018013000001”
7. total_fee 支付金额 单位:分,为了测试此值给1,表示支付1分钱
8. spbill_create_ip IP地址 网上很多ip的方法,自己找,此处测试给“127.0.0.1”
9. notify_url 回调地址:这是微信支付成功后,微信那边会带着一大堆参数(XML格式)请求这个地址多次,这个地址做我们业务处理如:修改订单状态,赠送积分等。Ps:支付还没成功还想这么远干嘛,最后再说。地址要公网可以访问。
10. trade_type 支付类型 咱们是公众号支付此处给“JSAPI”
11. openid 支付人的微信公众号对应的唯一标识,每个人的openid在不同的公众号是不一样的,这11个参数里,最费劲的就是他了,其他的几乎都已经解决,现在开发得到这个参数。
获得openid的部分内容应该不属于微信支付的范畴,属于微信公众号网页授权的东西,详情请参考微信网页授权:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
获得openid步骤:
第一步:用户同意授权,获取code
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
注意:1. redirect_uri参数:授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理。
2. scope:用snsapi_base 。
通过此链接可以获取code,可以在一个空页面设置一个a标签,链接至其redirect_uri的地址。点击a标签,即可链接到redirect_uri的地址,并携带code。
<a href="https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx15c*********&redirect_uri=http%3a%2f%2fwww.***.com%2fpay.jsp&response_type=code&cope=snsapi_base#wechat_redirect">去支付页面pay.jsp并携带code</a>
第二步:通过code换取网页授权access_token(其实微信支付就没有必要获取access_token了,咱们只要其中openid,不是要用户信息,此步结果已经就含有咱们需要的openid了)
获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
上一步的code有了,对于此链接的参数就容易了。可是在页面上如何处理是个问题,我是在pay.jsp页面加载完成后将获取code当做参数传异步到后台,在后台中用http相关类发送get请求(可以自行网上查找)。返回的JSON结果为:
{ "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID",//就是它,只要这个值 "scope":"SCOPE" }
好了,access_token是有了,不过咱们不关心它,咱们关心的是openid,有了它一就回到咱们“统一下单”接口里,所有的参数已经就位就等发送了。在回顾下11个必填参数:
1. appid APPID (已有)
2. mch_id 商户ID (已有)
3. nonce_str 随机字符串用WXPayUtil中的generateNonceStr()即可,就是生成UUID的方法;
4. sign 签名 用WXPayUtil中的publicstatic String generateSignature(final Map<String, String> data, Stringkey)方法,data是将除了sign外,其他10个参数放到map中,key是四大配置参数中的API秘钥(paternerKey)(此时可以处理它了,不过其他10个参数都有了,它就easy了,先new一个map,依次put其他10个参数,就可以用generateSignature方法了,得到了sign后,不要忘记再将sign put到只有10个参数的map中,这样才能凑齐最后的第11个参数。准备召唤神龙吧。);
5. body 所支付的名称
6. out_trade_no 自己后台生成的订单号,只要保证唯一就好:如“2018013000001”
7. total_fee 支付金额 单位:分,为了测试此值给1,表示支付1分钱
8. spbill_create_ip IP地址 网上很多ip的方法,自己找,此处测试给“127.0.0.1”
9. notify_url 回调地址:这是微信支付成功后,微信那边会带着一大堆参数(XML格式)请求这个地址多次,这个地址做我们业务处理如:修改订单状态,赠送积分等。Ps:支付还没成功还想这么远干嘛,最后再说。地址要公网可以访问。
10. trade_type 支付类型 咱们是公众号支付此处给“JSAPI”
11. openid (已有)
好了,准备工作完成,开始发送POST请求吧,上面提到网上找到的get请求的方法,此处用到post请求的方法,请求微信"统一下单接口https://api.mch.weixin.qq.com/pay/unifiedorder。发送前先用WXPayUtil工具类中的public static String mapToXml(Map<String,String> data)方法将有11个参数的map转成XML格式。发送后会返回String类型的返回值,如果你够幸运的话应该会得到XML的字符串:
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> <appid><![CDATA[wx2421b1c4370ec43b]]></appid> <mch_id><![CDATA[10000100]]></mch_id> <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str> <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid> <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign> <result_code><![CDATA[SUCCESS]]></result_code> <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id> <trade_type><![CDATA[JSAPI]]></trade_type> </xml>
如果你得到了以上的字符串,那么先恭喜你,坚持看到这,说明你的耐心还是不错的,因为“统一下单”接口调用完毕,可是并没有什么实际的效果,因为微信里想出现支付的界面是在前台完成的现在咱们还在后台玩耍,前面提到的我是页面加载完成时异步到后台的,咱们要返回异步的结果了,好了趁热继续吧。“统一下单”这么费劲的完成其实搞那么麻烦,就是为了得到上面红色的prepay_id(丫的,就这么一个参数给咱们搞的都想说***了)。
先用WXPayUtil类中的public static Map<String, String> xmlToMap(String strXML)方法,将刚才返回的XML格式的字符串转成map(为了方便取值)。map.get(“prepay_id”)就得到了prepay_id的值(比如得到的是:“wx2018…250…9981…666”),记住它,先保留此值。
看看前台都需要接收哪些值吧。
6个参数,咱们还是一个一个分析:
1. appId:四大参数之一的APPID;
2. timestamp:时间戳(newDate()即可)
3. nonceStr:随机字符串,再次用WXPayUtil中的generateNonceStr()即可;
4. package:就tm是它用到了prepay_id,但是还不是直接取值,还非要固定格式的,值的格式例如:”prepay_id= wx2018…250…9981…666”
5. signType:写MD5就好
6. paySign:又来了还是签名算法 ,按照上面的方法,用WXPayUtil中的publicstatic String generateSignature(final Map<String, String> data, Stringkey)方法,data是将除了paySign外,其他5个参数放到map中,key是四大配置参数中的API秘钥(paternerKey),得到了paySign后,不要忘记再将paySign put到只有5个参数的map中,这样才能凑齐最后的第6个参数。);
注意:此处有个小bug,很多人会被坑的很惨,不注意就掉坑里,我是掉进去了,就是最关键的第4个参数package,眼熟不眼熟,这tm是JAVA的关键字,不能用来当变量名。
所有的参数有了,返回给前端的方法有很多,简易用springMVC的@ResponseBody注解,即可将这个有6个参数的map按json格式传给前端。好了,后台工作完成。
前端的工作就容易多了,格式比较固定因为是微信固定格式,所以直接贴出我的代码,你只要更换触发支付的事件和异步的地址即可.
前端简单来说:1.一个空jsp页面上有个a标签,用来获取code,并跳转到pay.jsp(上面提到过)。
2.pay.jsp中需要异步到后台需要带code参数,pay.jsp中页面的地址上带着code,想获取code的方法很多,抛砖引引玉:(定义一个按钮,按钮上绑定一个code的属性值是页面链接的code的值,用EL表达式取的参数值,点击按钮触发点击事件)。
3.接收后台传过来值,调用固定方法。
Pay.jsp中内容只有一个”微信支付”的按钮,和js的代码,以下是js内容(获取code方法可以修改),其它内容不要修改
<!—pay.jsp中点击”微信支付”按钮执行pay()方法> <input id="code"type="button" value="微信支付"onclick="pay()" code="${param.code }"/> <script type="text/javascript"> var appId,timeStamp,nonceStr,package,signType,paySign; function pay(){ var code = $("#code").attr("code");//页面链接上的code参数 if(code){ var url = "http://异步地址?code="+code+"; $.get(url,function(result) { appId = result.appId; timeStamp = result.timeStamp; nonceStr = result.nonceStr; package = result.package; signType = result.signType; paySign = result.paySign; 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(); } }); } else { alert(“服务器错误”) } } function onBridgeReady(){ WeixinJSBridge.invoke( 'getBrandWCPayRequest', { "appId":appId, //公众号名称,由商户传入 "timeStamp":timeStamp, //时间戳,自1970年以来的秒数 "nonceStr":nonceStr, //随机串 "package":package, "signType":signType, //微信签名方式: "paySign":paySign //微信签名 }, function(res){ if(res.err_msg == "get_brand_wcpay_request:ok" ) { console.log('支付成功'); //支付成功后跳转的页面 }else if(res.err_msg == "get_brand_wcpay_request:cancel"){ console.log('支付取消'); }else if(res.err_msg == "get_brand_wcpay_request:fail"){ console.log('支付失败'); WeixinJSBridge.call('closeWindow'); } //使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。 }); } </script>
以下是后台部分
/** * @Description 微信浏览器内微信支付/公众号支付(JSAPI) * @param request * @param code * @return Map */ @RequestMapping(value="orders", method = RequestMethod.GET) @ResponseBody public Map orders(HttpServletRequest request,String code) { try { //页面获取openId接口 String getopenid_url = https://api.weixin.qq.com/sns/oauth2/access_token; String param= "appid="+你appid+"&secret="+你secret+"&code="+code+"&grant_type=authorization_code"; //向微信服务器发送get请求获取openIdStr String openIdStr = HttpRequest.sendGet(getopenid_url, param); JSONObject json = JSONObject.parseObject(openIdStr);//转成Json格式 String openId = json.getString("openid");//获取openId //拼接统一下单地址参数 Map<String, String> paraMap = new HashMap<String, String>(); //获取请求ip地址 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.getRemoteAddr(); } if(ip.indexOf(",")!=-1){ String[] ips = ip.split(","); ip = ips[0].trim(); } paraMap.put("appid", 你appid); paraMap.put("body", "尧舜商城-订单结算"); paraMap.put("mch_id", 你mchId); paraMap.put("nonce_str", WXPayUtil.generateNonceStr()); paraMap.put("openid", openId); paraMap.put("out_trade_no", 你的订单号);//订单号 paraMap.put("spbill_create_ip", ip); paraMap.put("total_fee",”1”); paraMap.put("trade_type", "JSAPI"); paraMap.put("notify_url",www.*******.com/***/**);// 此路径是微信服务器调用支付结果通知路径随意写 String sign = WXPayUtil.generateSignature(paraMap, paternerKey); paraMap.put("sign", sign); String xml = WXPayUtil.mapToXml(paraMap);//将所有参数(map)转xml格式 // 统一下单 https://api.mch.weixin.qq.com/pay/unifiedorder String unifiedorder_url = https://api.mch.weixin.qq.com/pay/unifiedorder; String xmlStr = HttpRequest.sendPost(unifiedorder_url, xml);//发送post请求"统一下单接口"返回预支付id:prepay_id //以下内容是返回前端页面的json数据 String prepay_id = "";//预支付id if (xmlStr.indexOf("SUCCESS") != -1) { Map<String, String> map = WXPayUtil.xmlToMap(xmlStr); prepay_id = (String) map.get("prepay_id"); } Map<String, String> payMap = new HashMap<String, String>(); payMap.put("appId", appid); payMap.put("timeStamp", WXPayUtil.getCurrentTimestamp()+""); payMap.put("nonceStr", WXPayUtil.generateNonceStr()); payMap.put("signType", "MD5"); payMap.put("package", "prepay_id=" + prepay_id); String paySign = WXPayUtil.generateSignature(payMap, paternerKey); payMap.put("paySign", paySign); return payMap; } catch (Exception e) { e.printStackTrace(); } return null; }
以下是网上找的一个发送get和post的类.仅供参考:提前导入相关jar包可网上查找(很容易)
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.URL; import java.net.URLConnection; import java.util.List; import java.util.Map; public class HttpRequest { /** * 向指定URL发送GET方法的请求 * * @param url * 发送请求的URL * @param param * 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 * @return URL 所代表远程资源的响应结果 */ public static String sendGet(String url, String param) { String result = ""; BufferedReader in = null; try { String urlNameString = url + "?" + param; System.out.println(urlNameString); URL realUrl = new URL(urlNameString); // 打开和URL之间的连接 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(); // 遍历所有的响应头字段 for (String key : map.keySet()) { System.out.println(key + "--->" + map.get(key)); } // 定义 BufferedReader输入流来读取URL的响应 in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("发送GET请求出现异常!" + e); e.printStackTrace(); } // 使用finally块来关闭输入流 finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); } } return result; } /** * 向指定 URL 发送POST方法的请求 * * @param url * 发送请求的 URL * @param param * 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 * @return 所代表远程资源的响应结果 */ public static String sendPost(String url, String param) { PrintWriter out = null; BufferedReader in = null; String result = ""; try { URL realUrl = new URL(url); // 打开和URL之间的连接 URLConnection conn = realUrl.openConnection(); // 设置通用的请求属性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // 发送POST请求必须设置如下两行 conn.setDoOutput(true); conn.setDoInput(true); // 获取URLConnection对象对应的输出流 out = new PrintWriter(conn.getOutputStream()); // 发送请求参数 out.print(param); // flush输出流的缓冲 out.flush(); // 定义BufferedReader输入流来读取URL的响应 in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("发送 POST 请求出现异常!"+e); e.printStackTrace(); } //使用finally块来关闭输出流、输入流 finally{ try{ if(out!=null){ out.close(); } if(in!=null){ in.close(); } } catch(IOException ex){ ex.printStackTrace(); } } return result; } }
----------------------------------------------------------------------------------
以下是微信支付成功后,向自己项目发送请求,自己来做一些业务处理。
/** * @ClassName WxPayController * @Description 微信支付成功后回调次接口 * @author wtao wangtao@eyaoshun.com * @date 2018年1月11日 下午3:10:59 */ //回调路径是自己在之前已经填写过的 @RequestMapping("/pay/") @Controller public class WxPayController { @Autowired private OrdersService ordersService; @Autowired private AccountService accountService; @Autowired private PointService pointService; @RequestMapping("callback") public String callBack(HttpServletRequest request,HttpServletResponse response){ //System.out.println("微信支付成功,微信发送的callback信息,请注意修改订单信息"); InputStream is = null; try { is = request.getInputStream();//获取请求的流信息(这里是微信发的xml格式所有只能使用流来读) String xml = WXPayUtil.inputStream2String(is, "UTF-8"); Map<String, String> notifyMap = WXPayUtil.xmlToMap(xml);//将微信发的xml转map if(notifyMap.get("return_code").equals("SUCCESS")){ if(notifyMap.get("result_code").equals("SUCCESS")){ String ordersSn = notifyMap.get("out_trade_no");//商户订单号 String amountpaid = notifyMap.get("total_fee");//实际支付的订单金额:单位 分 BigDecimal amountPay = (new BigDecimal(amountpaid).divide(new BigDecimal("100"))).setScale(2);//将分转换成元-实际支付金额:元 //String openid = notifyMap.get("openid"); //如果有需要可以获取 //String trade_type = notifyMap.get("trade_type"); /*以下是自己的业务处理------仅做参考 * 更新order对应字段/已支付金额/状态码 */ Orders order = ordersService.selectOrdersBySn(ordersSn); if(order != null) { order.setLastmodifieddate(new Date()); order.setVersion(order.getVersion().add(BigDecimal.ONE)); order.setAmountpaid(amountPay);//已支付金额 order.setStatus(2L);//修改订单状态为待发货 int num = ordersService.updateOrders(order);//更新order String amount = amountPay.setScale(0, BigDecimal.ROUND_FLOOR).toString();//实际支付金额向下取整-123.23--123 /* * 更新用户经验值 */ Member member = accountService.findObjectById(order.getMemberId()); accountService.updateMemberByGrowth(amount, member); /* * 添加用户积分数及添加积分记录表记录 */ pointService.updateMemberPointAndLog(amount, member, "购买商品,订单号为:"+ordersSn); } } } //告诉微信服务器收到信息了,不要在调用回调action了========这里很重要回复微信服务器信息用流发送一个xml即可 response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"); is.close(); } catch (Exception e) { e.printStackTrace(); } return null; } }
声明:此文章为转载文章