1、配置php
一、开通公众平台支付功能前端
微信支付功能先要申请微信(企业)公众平台,而后开通企业公众平台付功能。下图为微信(企业)公众平台页面,能够看到商户号等信息java
从开发-基本配置中获取APPIDgit
二、微信商户平台相关配置github
由于微信公众平台调整,公众平台微信支付公众号支付受权目录、扫码支付回调URL配置入口于2017年8月1日迁移至商户平台(pay.weixin.qq.com),因此微信支付配置和相关信息要登陆商户平台才能拿到。(估计是微信想要把公众号的管理功能和开发功能分离)web
从微信商户平台的产品中心-开发配置-支付配置配置扫码回调连接(扫码回调连接就是你项目中微信支付回调函数名称,这里须要的是加了项目域名的函数全称,必须保证能从公网访问。为何须要一个回调函数呢?这属于微信支付的回调机制:当用户使用微信支付完成后,你从本地是没法得知是否支付成功的,而微信这边在获取到支付完成的状态后,主动去访问你所设置的回调函数地址,将支付状态等相关信息返回,咱们只要在回调函数中判断支付状态,就可以便捷的进行下一步操做!)spring
微信sdk是微信官方给出的微信支付demo,其中有不少好用的工具类,demo自己也能够为咱们的支付接口开发提供参考(https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1)数据库
把sdk导入到项目中apache
2、开发后端
主要业务流程
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码连接code_url(code_url就是微信支付地址)。
(4)商户后台系统根据返回的code_url生成二维码(用第三方插件生成二维码)。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证连接有效性后发起用户支付,要求用户受权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交受权。
(8)微信支付系统根据用户受权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果经过短信、微信消息提示用户。微信客户端展现支付交易结果页面。
(10)微信支付系统经过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收状况,通知微信后台系统再也不发送该单的支付通知。
这是官方给出的文档,这里再梳理一下。单纯作PC端扫一扫开发很简单,主要是向微信支付的【统一下单API】请求,发送订单信息和签名(签名比较麻烦,可能前期测试会报屡次签名错误,不过官方SDK中有生成签名的方法,固然,本身也能够写),请求成功微信支付返回二维码连接code_url,注意这是微信支付的连接,不是二维码!不是二维码!不是二维码!二维码须要本身生成,不要直接就把code_url挂在页面上~
好了,上代码~
与支付无关的业务逻辑
这里我单首创建一个类PayController来写本身的业务逻辑,生成业务订单啊,业务订单保存在数据库啊,查询订单信息啊,验证是否支付完成啊等等,个人业务逻辑比较简单,仅供参考~
package com.xxx.controller; import com.xxx.pojo.ProductOrders; import com.xxx.service.ProOrdersService; import com.xxx.util.testPay; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.text.SimpleDateFormat; import java.util.*; /** * 与支付无关的业务逻辑 * */ @Controller public class PayController { @Resource private ProOrdersService proOrdersService;//订单增删查改的接口 /** * 调用接口 生成业务订单信息保存在数据库,并返回订单号 * * @param filetxt * @return ordernum */ @RequestMapping(value = "getOrder.do") @ResponseBody public Map getorder(HttpServletRequest request, @Param("filetxt") String filetxt) { Map<String, Object> map = new HashMap<>(); //获取当前用户 String username = (String) request.getSession().getAttribute("username"); if (username.isEmpty()) { map.put("type","2"); map.put("data","用户登录超时,请从新登录"); return map; } //订单对象,用户存储用户的订单信息,这里有些参数是请求【统一下单API】须要的,等我须要的时候就根据订单号从数据库取出相关参数 ProductOrders productOrders = new ProductOrders(); productOrders.setUserId(username);//用户 productOrders.setOrdernumber(getOutTradeNo());//订单号是随机生成的16位惟一字符串,用于匹配订单 productOrders.setProductId("XB001");//商品 int wordnum = filetxt.trim().length();//字数 productOrders.setQuantity(wordnum);//数量 Integer pay1 = testPay.getPay1(wordnum);//计算价格 productOrders.setTotalPrice(pay1);//总价 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式 String format = df.format(new Date());//日期格式转换 productOrders.setOrdertime(format); productOrders.setOrderDetails(filetxt);//文章内容 productOrders.setStatus(0); //设置订单详情格式 try { int insert = proOrdersService.insert(productOrders); } catch (Exception e) { System.out.println("Exception:添加订单异常"); e.printStackTrace(); } //封装返回值 map.put("orderid", productOrders.getOrdernumber());//订单号 return map; } /** * 查询订单信息 * * @param orderid * @return filetxt */ @RequestMapping(value = "selectOrder.do") @ResponseBody public Map selectOrder(@Param("orderid") String orderid) { ProductOrders productOrders = this.proOrdersService.selectByOrderId(orderid); Map<String, Object> map = new HashMap<>(); map.put("wordnum", productOrders.getQuantity()); map.put("totelprice", productOrders.getTotalPrice()); map.put("filetxt", productOrders.getOrderDetails()); return map; } /** * 验证支付状态,这个是查询是否支付完成的方法,微信在支付完成后访问了个人回调方法,修改数据库的订单状态 * 我经过此方法查询数据库中相关订单是否完成支付 * @Param orderid */ @RequestMapping(value = "OrderStatus.do") @ResponseBody public Map SelectOrderStatus(HttpServletRequest request, @Param("orderid") String orderid) { Map<String, Object> map = new HashMap<>(); int i = this.proOrdersService.selectOrderStatus(orderid); if (i == 1)//支付成功 { map.put("type", "SUCCESS"); return map; } map.put("type", "FAIL"); return map; } /** * 生成16位随机订单号 * @return key */ private static String getOutTradeNo() { SimpleDateFormat format = new SimpleDateFormat("MMddHHmmss", Locale.getDefault()); Date date = new Date(); String key = format.format(date); Random r = new Random(); key = key + r.nextInt(); key = key.replaceAll("-", "").substring(0, 15); return key; } }
微信支付逻辑
一、生成签名,而后打包成【统一下单API】要求格式的订单(参数列表),微信支付要求为XMl格式
二、调用【统一下单API】微信接口,将咱们打包好XMl格式的参数列表发送给【统一下单接口】,调用成功会接收到XMl格式的返回值,解析成咱们须要的格式,判断是否请求 成功,成功的话是会返回code_url的
三、而后咱们把code_url生成二维码展示给用户就OK了!
四、用户支付完成后,微信会访问咱们的回调接口,根据返回的结果修改数据库支付状态
将微信支付所须要的固定参数封装到类WXpayConfig中
package com.xxx.conf; public class WXpayConfig { public static String APPID = "wx830cXXXXXXX";//微信公众号APPID public static String WXPAYMENTACCOUNT = "xxxxxxxxxx";//微信公众号的商户号 public static String APIKEY = "xxxxxxxxxxx";//微信公众号的商户支付密钥 public static String basePath = "https://api.mch.weixin.qq.com/pay/unifiedorder";//统一下单请求地址 public static String notify_url = "http://www.xxxxx.com.cn/wxPayCallBack.do";//回调地址 }
import com.xxx.pojo.ProductOrders; import com.xxx.pojo.Products; import com.xxx.service.ProOrdersService; import com.xxx.service.ProductsService; import com.xxx.util.GetIPAdder; import com.xxx.util.QRCodeUtil; import com.xxx.conf.WXpayConfig; import com.github.wxpay.sdk.WXPayConstants; import com.github.wxpay.sdk.WXPayConstants.SignType; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.annotation.Resource; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.*; import static com.github.wxpay.sdk.WXPayUtil.*; @Controller public class WXPayController { @Resource private ProOrdersService proOrdersService;//订单操做接口 @Resource private ProductsService productsService;//产品操做接口 /** * 支付主接口,用于控制总体支付流程 * */ @RequestMapping(value = "pay") @ResponseBody public Map createQRCode(HttpServletRequest request, HttpServletResponse response, @Param("orderid") String orderid) { Map<String,String> map=new HashMap<>(); if (orderid.isEmpty()) { map.put("type","2"); map.put("data","订单号为空"); return map; } ServletOutputStream sos = null; try { String orderInfo = createOrderInfo(orderid);//生成【统一下单API】所需参数的接口 String code_url = httpOrder(orderInfo);//调用统一下单接口 sos = response.getOutputStream(); QRCodeUtil.encode(code_url, sos);//调用生成二维码的方法 } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return map; } /** * 生成统一下单格式的订单,生成一个XML格式的字符串 * @param orderId * @return */ private String createOrderInfo(String orderId) throws Exception { return createOrderInfo(orderId, 1); } private String createOrderInfo(String orderId, Integer productid) throws Exception { Products products = productsService.selectByPrimaryKey(Long.valueOf(productid));//商品对象 ProductOrders productOrders = this.proOrdersService.selectByOrderId(orderId);//订单信息 //生成订单对象 Map<String, String> map = new HashMap<>(); map.put("appid", WXpayConfig.APPID);//公众帐号ID map.put("mch_id", WXpayConfig.WXPAYMENTACCOUNT);//商户号 map.put("body", productOrders.getOrderDetails());//商品描述 map.put("nonce_str", generateUUID()); map.put("notify_url", WXpayConfig.notify_url);//通知地址 map.put("out_trade_no", orderId);//订单号 map.put("spbill_create_ip", GetIPAdder.getMyIP());//终端ip map.put("trade_type", "NATIVE");//交易类型 map.put("total_fee", String.valueOf(productOrders.getTotalPrice()));//总金额 String sign = createSign(map, WXpayConfig.APIKEY);//调用生成签名的方法,用以Map集合中的相关参数生成签名 map.put("sign", sign);//签名 //将订单对象转为xml格式 String s = null; try { return mapToXml(map);//maptoXml方法是微信sdk自带的方法 } catch (Exception e) { e.printStackTrace(); } return new String(s.getBytes("UTF-8")); } /** * 调统一下单API * @param orderInfo * @return */ private String httpOrder(String orderInfo) { String url = WXpayConfig.basePath; try { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); //加入数据 conn.setRequestMethod("POST"); conn.setDoOutput(true); BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream()); buffOutStr.write(orderInfo.getBytes("UTF-8")); buffOutStr.flush(); buffOutStr.close(); //获取输入流 BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); String line = null; StringBuffer sb = new StringBuffer(); while ((line = reader.readLine()) != null) { sb.append(line); } Map<String, String> map = xmlToMap(sb.toString()); //返回字段不少,这里只取咱们所需的字段 String return_msg = map.get("return_msg");//返回信息 String return_code = map.get("return_code");//状态码 String result_code = map.get("result_code");//业务结果 String code_url = map.get("code_url"); //根据微信文档return_code 和result_code都为SUCCESS的时候才会返回code_url if (null != map && "SUCCESS".equals(return_code) && "SUCCESS".equals(result_code)) { return code_url; } else { return null; } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 微信回调函数 * 支付成功后微信服务器会调用此方法,修改数据库订单状态 */ @RequestMapping(value = "/wxPayCallBack.do") @ResponseBody public String wxPayCallBack(HttpServletRequest request, HttpServletResponse response) { System.out.println("回调成功"); 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 result = new String(outSteam.toByteArray(), "utf-8");// 获取微信调用咱们notify_url的返回信息 Map<String, String> map = xmlToMap(result); if (map.get("result_code").equalsIgnoreCase("SUCCESS")) { //返回成功后修改订单状态 String out_trade_no = map.get("out_trade_no"); this.proOrdersService.updateByOrderId(out_trade_no); } } catch (Exception e) { } return "SUCCESS"; } /** * 生成签名 * 这个方法是从微信sdk里copy过来的,本身也能够写,要注意生成签名后UTF-8的转换,要否则容易报签名Body UTF-8错误 * @param data 待签名数据 * @param key API密钥 */ public static String createSign(final Map<String, String> data, String key) throws Exception { return createSign(data, key, SignType.MD5); } /** * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。 * * @param data 待签名数据 * @param key API密钥 * @param signType 签名方式 * @return 签名 */ private static String createSign(final Map<String, String> data, String key, SignType signType) throws Exception { //根据规则建立可排序的map集合 Set<String> keySet = data.keySet(); String[] keyArray = keySet.toArray(new String[keySet.size()]); Arrays.sort(keyArray); StringBuilder sb = new StringBuilder(); for (String k : keyArray) { if (k.equals(WXPayConstants.FIELD_SIGN)) { continue; } if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名 sb.append(k).append("=").append(data.get(k).trim()).append("&"); } sb.append("key=").append(key); //转换UTF-8 String str = new String(sb.toString().getBytes("UTF-8")); if (WXPayConstants.SignType.MD5.equals(signType)) { return MD5(sb.toString()).toUpperCase(); } else if (WXPayConstants.SignType.HMACSHA256.equals(signType)) { return HMACSHA256(sb.toString(), key); } else { throw new Exception(String.format("Invalid sign_type: %s", signType)); } } }
若是请求【统一下单接口】的参数正确,签名也没有报错,那咱们就能成功获取到code_url,从而生成二维码,让用户扫码支付了。
使用了第三方工具类zxing,这里用到的zxing依赖包请自行下载
package com.xxx.util; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.OutputStream; import java.util.Hashtable; import java.util.Random; /** * 二维码工具类 * */ public class QRCodeUtil { private static final String CHARSET = "utf-8"; private static final String FORMAT_NAME = "JPG"; // 二维码尺寸 private static final int QRCODE_SIZE = 300; // LOGO宽度 private static final int WIDTH = 60; // LOGO高度 private static final int HEIGHT = 60; private static BufferedImage createImage(String content, String imgPath, boolean needCompress) throws Exception { Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>(); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); hints.put(EncodeHintType.CHARACTER_SET, CHARSET); hints.put(EncodeHintType.MARGIN, 1); BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,hints); int width = bitMatrix.getWidth(); int height = bitMatrix.getHeight(); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); } } if (imgPath == null || "".equals(imgPath)) { return image; } // 插入图片 QRCodeUtil.insertImage(image, imgPath, needCompress); return image; } /** * 插入LOGO * * @param source * 二维码图片 * @param imgPath * LOGO图片地址 * @param needCompress * 是否压缩 * @throws Exception */ private static void insertImage(BufferedImage source, String imgPath, boolean needCompress) throws Exception { File file = new File(imgPath); if (!file.exists()) { System.err.println("" + imgPath + " 该文件不存在!"); return; } Image src = ImageIO.read(new File(imgPath)); int width = src.getWidth(null); int height = src.getHeight(null); if (needCompress) { // 压缩LOGO if (width > WIDTH) { width = WIDTH; } if (height > HEIGHT) { height = HEIGHT; } Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH); BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = tag.getGraphics(); g.drawImage(image, 0, 0, null); // 绘制缩小后的图 g.dispose(); src = image; } // 插入LOGO Graphics2D graph = source.createGraphics(); int x = (QRCODE_SIZE - width) / 2; int y = (QRCODE_SIZE - height) / 2; graph.drawImage(src, x, y, width, height, null); Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6); graph.setStroke(new BasicStroke(3f)); graph.draw(shape); graph.dispose(); } /** * 生成二维码(内嵌LOGO) * * @param content * 内容 * @param imgPath * LOGO地址 * @param destPath * 存放目录 * @param needCompress * 是否压缩LOGO * @throws Exception */ public static void encode(String content, String imgPath, String destPath, boolean needCompress) throws Exception { BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress); mkdirs(destPath); String file = new Random().nextInt(99999999) + ".jpg"; ImageIO.write(image, FORMAT_NAME, new File(destPath + "/" + file)); } /** * 当文件夹不存在时,mkdirs会自动建立多层目录,区别于mkdir.(mkdir若是父目录不存在则会抛出异常) * * @author lanyuan Email: mmm333zzz520@163.com * @date 2013-12-11 上午10:16:36 * @param destPath * 存放目录 */ public static void mkdirs(String destPath) { File file = new File(destPath); // 当文件夹不存在时,mkdirs会自动建立多层目录,区别于mkdir.(mkdir若是父目录不存在则会抛出异常) if (!file.exists() && !file.isDirectory()) { file.mkdirs(); } } /** * 生成二维码(内嵌LOGO) * * @param content * 内容 * @param imgPath * LOGO地址 * @param destPath * 存储地址 * @throws Exception */ public static void encode(String content, String imgPath, String destPath) throws Exception { QRCodeUtil.encode(content, imgPath, destPath, false); } /** * 生成二维码 * * @param content * 内容 * @param destPath * 存储地址 * @param needCompress * 是否压缩LOGO * @throws Exception */ public static void encode(String content, String destPath, boolean needCompress) throws Exception { QRCodeUtil.encode(content, null, destPath, needCompress); } /** * 生成二维码 * * @param content * 内容 * @param destPath * 存储地址 * @throws Exception */ public static void encode(String content, String destPath) throws Exception { QRCodeUtil.encode(content, null, destPath, false); } /** * 生成二维码(内嵌LOGO) * * @param content * 内容 * @param imgPath * LOGO地址 * @param output * 输出流 * @param needCompress * 是否压缩LOGO * @throws Exception */ public static void encode(String content, String imgPath, OutputStream output, boolean needCompress) throws Exception { BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress); ImageIO.write(image, FORMAT_NAME, output); } /** * 生成二维码 * * @param content * 内容 * @param output * 输出流 * @throws Exception */ public static void encode(String content, OutputStream output) throws Exception { QRCodeUtil.encode(content, null, output, false); } public static void main(String[] args) throws Exception { String text = "test"; QRCodeUtil.encode(text, "/Users/noahshen/Downloads/6BFAADD4-256D-447B-B742-1E1DFF11094F_meitu_1.png", "/Users/noahshen/Downloads", true); // QRCodeUtil.encode(text, null, "/Users/noahshen/Downloads", true); } }
当用户支付完成后,微信成功调用了咱们的回调方法,数据库订单状态修改成“已支付”,Java后端的工做就基本完成了,那前端怎么知道用户完成了支付呢?如今广泛的办法是,前端写方法轮询支付状态,限定时间内查询到支付状态为“已支付”就进行下一步操做,限定时间后未支付就作支付超时的操做。本项目用户查询支付状态的代码已经写在了以前“与支付无关的业务逻辑”中了~