您现在的位置是:首页 > 文章详情

Java微信扫码支付

日期:2018-09-10点击:371
版权声明:本文首发 http://asing1elife.com ,转载请注明出处。 https://blog.csdn.net/asing1elife/article/details/82620021

Java微信扫码支付

以下内容是基于模式二开发
在开发之前需要先到微信支付官网注册账号,并获取到以下信息
appid:wx1137939101111111公众账号id
mch_id:1438111111 商户号
key:4Inn0va1eSxOnl1neqsxwuhan1111111密钥
send_url:https://api.mch.weixin.qq.com/pay/unifiedorder统一下单API
notify_url:http://127.0.0.1:8080/sop/order/notify/wechat支付成功回调地址

更多精彩

官网

  1. 微信支付官网
  2. 扫码支付开发者文档

模式

  1. 需要在公众平台后台设置支付回调URL ,用于接收用户扫码后微信支付系统回调的productid和openid
  2. 直接调用统一下单API 即可,相对于模式一更为简洁

定义接口对象

  1. 根据 统一下单接口API 定义四个对象,用于发送和接收数据
    UnifiedOrderRequest.java 统一下单请求参数-必填项
public class UnifiedOrderRequest { // 公众账号id private String appid; // 商户号 private String mch_id; // 随机字符串,32位以内 private String nonce_str; // 签名,遵循签名算法 private String sign; // 商品描述,浏览器打开的网站主页title名称-商品概述 private String body; // 商户订单号,32位以内,不重复 private String out_trade_no; // 标价金额,单位分 private Integer total_fee; // 终端ip,填写调用端的ip private String spbill_create_ip; // 通知地址,接收支付结果的会掉地址,必须外网可访问 private String notify_url; // 交易类型,JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付 private String trade_type; }

UnifiedOrderRequestExt.java 统一下单请求参数-非必填项

public class UnifiedOrderRequestExt extends UnifiedOrderRequest { // 设备号,网页端填写WEB private String device_info; // 签名类型,默认MD5 private String sign_type; // 商品详情,JSON格式 private String detail; // 附加数据,可作为自定义参数使用 private String attach; // 标价币种,默认CNY private String fee_type; // 交易起始时间,格式为yyyyMMddHHmmss private String time_start; // 交易结束时间,最短失效时间必须间隔5分钟 private String time_expire; // 商品id,trade_type=NATIVE时,必填 private String product_id; // 指定支付方式,no_credit可限制使用信用卡 private String limit_pay; // 用户标识,trade_type=JSAPI时,必填 private String openid; }

UnifiedOrderResponse.java 统一下单返回参数-必填项

public class UnifiedOrderResponse { // 返回状态码,通信标识,SUCCESS/FAIL private String return_code; // 公众账号id private String appid; // 商户号 private String mch_id; // 随机字符串 private String nonce_str; // 签名 private String sign; // 业务结果,交易标识,SUCCESS/FAIL private String result_code; // 交易类型,JSAPI,NATIVE,APP private String trade_type; // 预支付交易会话标识,有效值2小时 private String prepay_id; }

UnifiedOrderResponseExt.java 统一下单返回参数-非必填项

public class UnifiedOrderResponseExt extends UnifiedOrderResponse { // 返回信息,非空则表示返回了错误信息 private String return_msg; // 设备号 private String device_info; // 错误代码 private String err_code; // 错误代码描述 private String err_code_des; // 二维码连接,trade_type=NATIVE时返回 private String code_url; }

定义一个标签用于显示二维码

  • 调用统一下单API成功后,会返回一系列XML数据,其中code_url表示返回的预支付交易链接,可将其生成二维码图片
<div class="order-pay-panel order-wechat-panel"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title"> <i class="icon-th-large"></i> 微信支付 </h4> </div> <div class="modal-body"> <div class="wechat-qrcode-panel margin-bottom-10"> <img src="${ctx}/api/order/pay/wechat/qrcode?orderId=${order.hexId}"> </div> <div class="wechat-description-panel"> <p class="text-muted">使用微信扫描二维码完成支付</p> <p class="text-danger">¥${order.price}</p> </div> </div> </div>

根据统一下单API的要求生成订单

  • 将系统内部订单号传入请求参数的out_trade_no中,用于后续操作的唯一标识符
  • 请求参数中的sign是验证参数合法性的唯一标识,需要根据 微信支付签名算法 来生成
  • 使用XStream将对象转换为XML,由于微信的请求参数中大量使用下划线,但下划线在XStream中是关键字,因此需要把下划线转换为双下划线,避免报错
private String generateOrderInfo(Long orderId) throws Exception { // 获取订单信息 OrderDTO order = orderManageService.getOrder(orderId); // 生成订单 UnifiedOrderRequestExt ext = new UnifiedOrderRequestExt(); ext.setAppid(SOPConstants.WECHAT_PAY_APP_ID); ext.setMch_id(SOPConstants.WECHAT_PAY_MCH_ID); ext.setBody("轻实训-" + order.getName()); ext.setOut_trade_no(order.getCode()); ext.setTotal_fee(order.getPrice() * 100); ext.setSpbill_create_ip(super.getClientIP()); ext.setNotify_url(SOPConstants.WECHAT_PAY_NOTIFY_URL); ext.setTrade_type("NATIVE"); ext.setProduct_id(order.getHexId()); // 生成32位随机数 ext.setNonce_str(makeNonceStr()); // 签名,按照指定签名算法生成 ext.setSign(makeSign(ext)); // 格式转换为XML XStream xStream = new XStream(new XppDriver(new XmlFriendlyReplacer("_-", "_"))); xStream.alias("xml", UnifiedOrderRequestExt.class); return xStream.toXML(ext); }
  • 生成32位随机数,方式为当前时间加随机数
private String makeNonceStr() { StringBuffer str = new StringBuffer(DateUtil.getSysDateString("yyyyMMddHHmmssS")); str.append((new Random().nextInt(900) + 100)); return str.toString(); }
  • 拼接签名数据
private String makeSign(UnifiedOrderRequestExt ext) throws Exception { // 根据规则创建可排序的map集合 SortedMap<String, String> signMaps = Maps.newTreeMap(); signMaps.put("appid", ext.getAppid()); signMaps.put("body", ext.getBody()); signMaps.put("mch_id", ext.getMch_id()); signMaps.put("nonce_str", ext.getNonce_str()); signMaps.put("notify_url", ext.getNotify_url()); signMaps.put("out_trade_no", ext.getOut_trade_no()); signMaps.put("spbill_create_ip", ext.getSpbill_create_ip()); signMaps.put("trade_type", ext.getTrade_type()); signMaps.put("total_fee", ext.getTotal_fee().toString()); signMaps.put("product_id", ext.getProduct_id()); // 生成签名 return generateSign(signMaps); }
  • 按照签名算法生成签名
private String generateSign(SortedMap<String, String> signMaps) throws Exception { StringBuffer sb = new StringBuffer(); // 字典序 for (Map.Entry signMap : signMaps.entrySet()) { String key = (String) signMap.getKey(); String value = (String) signMap.getValue(); // 为空不参与签名、参数名区分大小写 if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) { sb.append(key + "=" + value + "&"); } } // 拼接key sb.append("key=" + SOPConstants.WECHAT_PAY_KEY); // MD5加密 return encoderByMd5(sb.toString()).toUpperCase(); }

调用统一下单API

  • 将生成的订单发送给微信,同时接收微信的返回参数,读取其中的code_url
  • 如果发送的订单信息不符合要求,则会在返回参数中告知问题
  • 订单合法,返回参数中return_code=SUCCESS return_msg=OK result_code=SUCCESS
  • 订单不合法,返回参数中return_code=FAIL return_msg=具体错误原因
private String sendHttpRequest(String orderInfo) throws IOException { // 建立连接 HttpURLConnection conn = (HttpURLConnection) new URL(SOPConstants.WECHAT_PAY_SEND_URL).openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); // 发送数据 BufferedOutputStream bos = new BufferedOutputStream(conn.getOutputStream()); bos.write(orderInfo.getBytes()); bos.flush(); bos.close(); // 获取数据 BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); // 接收数据 String line; StringBuffer str = new StringBuffer(); while ((line = reader.readLine()) != null) { str.append(line); } // XML数据转换为对象 XStream xStream = new XStream(new XppDriver(new XmlFriendlyReplacer("_-", "_"))); xStream.alias("xml", UnifiedOrderResponseExt.class); UnifiedOrderResponseExt ext = (UnifiedOrderResponseExt) xStream.fromXML(str.toString()); // 判断数据有效性 if (null != ext && "SUCCESS".equals(ext.getReturn_code()) && "SUCCESS".equals(ext.getResult_code())) { return ext.getCode_url(); } return null; }

根据返回的code_url生成二维码图片

@RequestMapping(value = “/wechat/qrcode”, method = RequestMethod.GET) public void wechatQRCode(HttpServletResponse response, @RequestParam("orderId") String orderId) { try { // 初始化数据 int width = 240; int height = 240; String format = "png"; // 获取二维码链接 String codeUrl = orderPayService.getQRCodeUrl(IdEncoder.decodeId(orderId)); Hashtable htable = new Hashtable(); htable.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 生成图片 BitMatrix matrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE, width, height, htable); OutputStream out = response.getOutputStream(); // 输出图片 MatrixToImageWriter.writeToStream(matrix, format, out); out.flush(); out.close(); } catch (Exception e) { logger.error(e.getMessage(), e); } }
  • 需要准备的包信息
<dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.3.0</version> </dependency>

接收回调

  • 用户通过微信扫描二维码并支付成功后,微信会根据之前订单中的notify_url回调地址进行回执
  • 此处提供给微信的回调地址必须是外网可访问的,否则无法正常接收回执信息
  • 由于回执时并没有携带用户信息,所以如果使用了诸如shiro等安全框架的,需要给予该回执地址一个访问许可,否则会被安全框架屏蔽
  • 发送回执是异步进行,由于网络等不确定因素,微信不保证回执一定成功
  • 微信会通过一定的策略定期重启发送通知,通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒
  • 虽然是异步回执,但并不需要采用ajax异步接收的方式来接收数据
@RequestMapping(value = "/wechat") public String wechatNotify(HttpServletResponse response, HttpServletRequest request) throws Exception { orderPayService.notify(response, request); // 此处的返回值无效,需要在支付页面通过轮询获取支付结果,微信支付本身无法实现自动跳转 return "/redirect:/login"; }

处理回执内容

  • 数据是通过IO流发送,所以也需要通过IO流接收
  • 微信发送回执用户接收后,需要通过IO流的方式告知微信接收成功,否则微信认为回执失败
  • 接收到回执信息后,最关键是验证签名来确保信息的有效性和安全性,验签的方式和发送订单签名的方式一致
  • 验签成功,且回执信息中result_code=SUCCESS,则表示回执信息有效
  • 从回执信息中可获取到out_trade_no,这是之前发送的用户订单唯一标识符,通过该信息可以继续处理用户订单
  • 所有流程处理完毕后,必须以XML格式编写回执信息,并通过IO流的方式告知微信回执接收成功
public void notify(HttpServletResponse response, HttpServletRequest request) throws Exception { // 读取回执数据 HashMap<String, String> notifyMaps = readNotify(request); // 回执数据验证 if (notifyMaps == null || notifyMaps.isEmpty()) { logger.error("未收到回执数据!"); throw new TSharkException("未收到回执数据!"); } // 挑选数据 SortedMap<String, String> notifySorts = sortNotify(notifyMaps); // 重新签名 String sign = generateSign(notifySorts); // 获取回执签名 String notifySign = notifySorts.get("sign").toUpperCase(); // 验证签名 if (!sign.equals(notifySign)) { logger.error("签名验证失败!"); throw new TSharkException("签名验证失败!"); } String resXml; // 验证回执 if ("SUCCESS".equals(notifySorts.get("result_code"))) { // 更新订单信息 updateOrderInfo(notifySorts.get("out_trade_no")); // 微信回执 resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> "; } else { resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> "; } BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); }
  • 从IO流中读取回执信息
private HashMap<String, String> readNotify(HttpServletRequest request) throws Exception { // 读取参数 InputStream inputStream = request.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); // 接收数据 String line; StringBuffer str = new StringBuffer(); while ((line = reader.readLine()) != null) { str.append(line); } reader.close(); inputStream.close(); // XML转换Map return fromXml(str.toString()); }
  • 读取的回执信息时XML格式,需要通过jDom的SAXBuilder解析为Map
private HashMap<String, String> fromXml(String xml) throws Exception { xml = xml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\""); if (null == xml || "".equals(xml)) { return null; } HashMap<String, String> m = Maps.newHashMap(); InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8")); 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 = getXmlChildren(children); } m.put(k, v); } //关闭流 in.close(); return m; } private String getXmlChildren(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(getXmlChildren(list)); } sb.append(value); sb.append("</" + name + ">"); } } return sb.toString(); }

轮询订单状态,实现支付完成后页面自动跳转

  • 由于支付回执是异步的,所以即使捕获到异步回执也无法实现支付页面的自动跳转
  • 所以需要在支付页面打开时设置一个ajax轮询订单状态,一旦订单状态更新,则进行页面跳转
// 页面关闭 $(".modal-header button.close:last").click(function () { // 停止轮询 clearInterval(checkTimer); }); // 轮询订单状态 checkTimer = setInterval(function () { // 窗口是否打开 if ($(".order-pay-panel").length <= 0) { clearInterval(checkTimer); } // 获取订单状态 $.ts.doAction("/api/order/review/check/", { orderId: orderId }, function () { // 订单已支付 if (!this.data) { $.ts.closeWindow(); g_index.loadMainContentWithState("/order/manage"); $.ts.toastr.success("订单已支付成功!"); } }, "", "", ""); }, 3000);
原文链接:https://yq.aliyun.com/articles/646438
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章