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後端的工做就基本完成了,那前端怎麼知道用戶完成了支付呢?如今廣泛的辦法是,前端寫方法輪詢支付狀態,限定時間內查詢到支付狀態爲「已支付」就進行下一步操做,限定時間後未支付就作支付超時的操做。本項目用戶查詢支付狀態的代碼已經寫在了以前「與支付無關的業務邏輯」中了~