前邊講過了 微信的掃一掃登陸功能實戰
今天繼續實戰一下微信的掃一掃支付功能實戰
一、準備
我們想接入微信的掃一掃支付功能,那首先需要開通微信的商戶平臺,然後申請開通支付
本篇是側重講我們在代碼裏怎麼接入微信的支付功能,所以具體怎麼申請開通微信支付就不細講了。
申請微信商家賬號和開通支付功能,我們主要是爲了拿到兩個屬性:一個是微信支付商戶號,另一個是微信支付API祕鑰。
如果是你們公司需要你開發代碼接入微信支付功能,那這兩個信息你們公司肯定會提前申請好提供給你的,要不然就沒辦法開發聯調測試了,所以公司肯定會提前給你的,個人沒必要太關注,當然你自己感興趣辦一個營業執照做個網站想接入的話,也可以研究一下。我這邊就是自己的營業執照申請的,按照提示一步一步做,也不難。
申請好了進入一下頁面拿到上邊說的兩個
二、瞭解整體過程
我們可以先看下微信官方給的接入文檔
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=2_2
打開文檔我們看到,微信支付的文檔還是比較多的,看完文檔後覺得裏邊最重要的是一張官方介紹的業務流程時序圖和API列表裏邊的幾個重要的接口文檔
下面我們先看下微信官方提供的時序圖
仔細看完這張時序圖(其中圖上的紅色部分是我們需要做的)
我們就能從整體上了解整個過程:
a、用戶去我們的網站選擇了一個商品點擊購買,然後訪問我們後臺的接口,這個接口需要處理的邏輯是首先在我們系統裏生成一條訂單,然後調用微信的統一下單接口,微信的統一下單接口需要根據文檔上指定的一些參數進行生成簽名和加密傳輸,具體的簽名生成規則可以參考下邊代碼的 WXPayUtil 類裏的 createSign 方法或者可以參考官方的這個鏈接:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
我們按照規則生成簽名和加密串後,就可以調用微信的統一下單接口了,微信後臺系統會給我們返回一個code_url鏈接,我們後臺的代碼拿到這個code_url鏈接後將它轉成二維碼圖片,返給前端展示,然後用戶使用微信掃一掃進行支付;
b、用戶支付後,微信會回調我們的接口,告訴我們支付的結果,所以我們需要再寫一個微信支付回調的接口
這個接口的大概邏輯應該是:拿到支付成功的狀態去更新我們系統裏訂單表裏的狀態爲已支付,然後給微信返回我們已經收到通知的信息;如果我們的接口沒給微信返回已經收到通知的信息,微信那邊會有一定的策略,它會每隔一段時間去調一次我們的接口試一下,直到成功或者直到達到一定的次數或者達到一定的時間,纔會停止調用
還有就是我們這邊超過一定的時間沒收到微信的回調的話,我們也可以主動調微信的接口去查詢相應的支付狀態
下圖是微信官方給出的回調時的步驟和注意事項
三、開始實戰開發
瞭解完文檔後,我就可以開始開發了
我們整體上需要兩個接口:用戶點擊下單我們調微信系統生成二維碼的接口和微信支付的回調接口
1、下單生成二維碼的接口
首先把微信支付商戶號和微信支付API祕鑰配置到配置文件裏
然後開始開發,我們以用戶購買我們網站的學習視頻爲例進行講解
package com.cj.wx_pay.controller;
import com.cj.wx_pay.domain.JsonData;
import com.cj.wx_pay.domain.Video;
import com.cj.wx_pay.domain.VideoOrder;
import com.cj.wx_pay.dto.VideoOrderDto;
import com.cj.wx_pay.service.VideoOrderService;
import com.cj.wx_pay.service.VideoService;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* 訂單接口
*/
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {
@Autowired
private VideoOrderService videoOrderService;
@Autowired
private VideoService videoService;
//random參數是一個隨機的且唯一的字符串,作用是供前端輪詢查詢訂單狀態使用的,暫時可以先忽略測
//試的時候隨便傳入一個字符串就可以
@GetMapping("add")
public void saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
@RequestParam(value = "random",required = true)String random,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
//String ip = IpUtils.getIpAddr(request);
//int userId = request.getAttribute("user_id");
int userId = 1; //臨時寫死的配置,實際應該從用戶帶的token裏進行解析然後讀取
String ip = "120.25.1.43";
VideoOrderDto videoOrderDto = new VideoOrderDto();
videoOrderDto.setUserId(userId);
videoOrderDto.setVideoId(videoId);
videoOrderDto.setIp(ip);
videoOrderDto.setRandom(random);
String codeUrl = videoOrderService.save(videoOrderDto);
if(codeUrl == null) {
throw new NullPointerException();
}
try{
Cookie ck = new Cookie("trande_no",videoOrderDto.getOutTradeNo());
ck.setMaxAge(1000);
response.addCookie(ck);
//生成二維碼配置
Map<EncodeHintType,Object> hints = new HashMap<>();
//設置糾錯等級
hints.put(EncodeHintType.ERROR_CORRECTION,ErrorCorrectionLevel.L);
//編碼類型
hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl,BarcodeFormat.QR_CODE,400,400,hints);
OutputStream out = response.getOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix,"png",out);
}catch (Exception e){
e.printStackTrace();
}
}
}
package com.cj.wx_pay.service.impl;
import com.cj.wx_pay.config.WeChatConfig;
import com.cj.wx_pay.domain.User;
import com.cj.wx_pay.domain.Video;
import com.cj.wx_pay.domain.VideoOrder;
import com.cj.wx_pay.dto.VideoOrderDto;
import com.cj.wx_pay.mapper.UserMapper;
import com.cj.wx_pay.mapper.VideoMapper;
import com.cj.wx_pay.mapper.VideoOrderMapper;
import com.cj.wx_pay.service.VideoOrderService;
import com.cj.wx_pay.utils.CommonUtils;
import com.cj.wx_pay.utils.HttpUtils;
import com.cj.wx_pay.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
@Service
public class VideoOrderServiceImpl implements VideoOrderService {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private VideoMapper videoMapper;
@Autowired
private VideoOrderMapper videoOrderMapper;
@Autowired
private UserMapper userMapper;
/**
* 下單接口
* @param videoOrderDto
* @return
* @throws Exception
*/
@Override
@Transactional(propagation = Propagation.REQUIRED)
public String save(VideoOrderDto videoOrderDto) throws Exception {
//查找視頻信息
Video video = videoMapper.findById(videoOrderDto.getVideoId());
//查找用戶信息
User user = userMapper.findByid(videoOrderDto.getUserId());
//生成訂單
VideoOrder videoOrder = new VideoOrder();
videoOrder.setTotalFee(video.getPrice());
videoOrder.setVideoImg(video.getCoverImg());
videoOrder.setVideoTitle(video.getTitle());
videoOrder.setCreateTime(new Date());
videoOrder.setVideoId(video.getId());
videoOrder.setState(0);
videoOrder.setUserId(user.getId());
videoOrder.setHeadImg(user.getHeadImg());
videoOrder.setNickname(user.getName());
videoOrder.setDel(0);
videoOrder.setIp(videoOrderDto.getIp());
videoOrder.setOutTradeNo(CommonUtils.generateUUID());
videoOrder.setRandom(videoOrderDto.getRandom());
videoOrderMapper.insert(videoOrder);
//微信的統一下單方法,獲取codeurl
String codeUrl = unifiedOrder(videoOrder);
return codeUrl;
}
/**
* 微信的統一下單方法
* 生成微信支付sign,首先需要傳入一個按照字典序排列好的map,然後拼接生成一個按字典序排列的參
* 數,最後再拼接上微信支付後臺拿到的祕鑰參數,就可以生成一個微信統一下單接口那邊需要的sign
* 簽名了。然後把這個sign再拼接上微信需要的其他參數,並組織成一個XML格式的數據傳給微信統一下
* 單接口就OK了
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {
//int i = 1/0; //模擬異常
//生成簽名
SortedMap<String,String> params = new TreeMap<>();
params.put("appid",weChatConfig.getAppId());
params.put("mch_id", weChatConfig.getMchId());
params.put("nonce_str",CommonUtils.generateUUID());
params.put("body",videoOrder.getVideoTitle());
params.put("out_trade_no",videoOrder.getOutTradeNo());
params.put("total_fee",videoOrder.getTotalFee().toString());
params.put("spbill_create_ip",videoOrder.getIp());
params.put("notify_url",weChatConfig.getPayCallbackUrl());
params.put("trade_type","NATIVE");
//sign簽名
String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
params.put("sign",sign);
//map轉xml
String payXml = WXPayUtil.mapToXml(params);
//System.out.println(payXml);
//統一下單
String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
if(null == orderStr) {
return null;
}
Map<String, String> unifiedOrderMap = WXPayUtil.xmlToMap(orderStr);
//System.out.println(new String(unifiedOrderMap.toString().getBytes("ISO-8859-1"), "UTF-8"));
if(unifiedOrderMap != null) {
return unifiedOrderMap.get("code_url");
}
return null;
}
}
生成簽名的具體步驟可以看下邊的這個工具類
package com.cj.wx_pay.utils;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.*;
/**
* 微信支付工具類,xml轉map,map轉xml,生成簽名
*/
public class WXPayUtil {
/**
* XML格式字符串轉換爲Map
*
* @param strXML XML字符串
* @return XML數據轉換後的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
throw ex;
}
}
/**
* 將Map轉換爲XML格式的字符串
*
* @param data Map類型數據
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
/**
* 生成微信支付sign,首先需要傳入一個按照字典序排列好的map,然後拼接生成一個按字典序排列的參
* 數,最後再拼接上微信支付後臺拿到的祕鑰參數,就可以生成一個微信統一下單接口那邊需要的sign
* 簽名了。然後把這個sign再拼接上微信需要的其他參數,並組織成一個XML格式的數據傳給微信統一下
* 單接口就OK了
* @return
*/
public static String createSign(SortedMap<String, String> params, String key){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String,String>> it = es.iterator();
//生成 類似這個的一個字符串stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
while (it.hasNext()){
Map.Entry<String,String> entry = (Map.Entry<String,String>)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
sb.append(k+"="+v+"&");
}
}
sb.append("key=").append(key);
String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
return sign;
}
/**
* 校驗簽名
* @param params
* @param key
* @return
*/
public static boolean isCorrectSign(SortedMap<String, String> params, String key){
String sign = createSign(params,key);
String weixinPaySign = params.get("sign").toUpperCase();
return weixinPaySign.equals(sign);
}
/**
* 獲取有序map
* @param map
* @return
*/
public static SortedMap<String,String> getSortedMap(Map<String,String> map){
SortedMap<String, String> sortedMap = new TreeMap<>();
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()){
String key = (String)it.next();
String value = map.get(key);
String temp = "";
if( null != value){
temp = value.trim();
}
sortedMap.put(key,temp);
}
return sortedMap;
}
}
第一個調用微信統一下單接口生成我們需要的二維碼的接口就搞定了,下面我們先測試一下
可以看到訪問我們的下單接口就回返回一個微信支付的二維碼,我們掃一掃測試一下
可以看到已經支付成功了
我們進入我們微信商家之後後臺看下,錢已經到賬了
用戶支付後,微信需要調我們提供的接口來告訴我們用戶支付成功了,然後我們去更新我們本地的訂單狀態爲已支付,然後給用戶發貨啊等等操作。所以下邊我們寫一下微信回調我們的接口
2、微信支付回調接口
/**
* 微信支付回調接口
*/
@RequestMapping("/order/callback")
public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
InputStream inputStream = request.getInputStream();
//BufferedReader是包裝設計模式,BufferedReader帶緩衝而且可以一行一行的讀,性能更高
//stream和reader之間的轉換需要一個轉換流,InputStreamReader
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
StringBuffer sb = new StringBuffer();
String line;
while ((line = in.readLine()) != null){
sb.append(line);
}
in.close();
inputStream.close();
Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());
//System.out.println(callbackMap.toString());
SortedMap<String,String> sortedMap = WXPayUtil.getSortedMap(callbackMap);
//判斷簽名是否正確
if(WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey())){
if("SUCCESS".equals(sortedMap.get("result_code"))){
String outTradeNo = sortedMap.get("out_trade_no");
VideoOrder dbVideoOrder = videoOrderService.findByOutTradeNo(outTradeNo);
if(dbVideoOrder != null && dbVideoOrder.getState()==0){ //判斷邏輯看業務場景
VideoOrder videoOrder = new VideoOrder();
videoOrder.setOpenid(sortedMap.get("openid"));
videoOrder.setOutTradeNo(outTradeNo);
videoOrder.setNotifyTime(new Date());
videoOrder.setState(1);//更新訂單狀態爲已支付
int rows = videoOrderService.updateVideoOderByOutTradeNo(videoOrder);
if(rows == 1){ //通知微信訂單處理成功
response.setContentType("text/xml");
response.getWriter().println("success");
return;
}
}
}
}
//處理失敗給微信返回fail
response.setContentType("text/xml");
response.getWriter().println("fail");
}
這樣我們的接口就寫完了
下面我們加上簡單的前端代碼,測試一下(前端頁面的代碼就不貼上來了,大家可以根據自己的實際項目情況去做)
點擊購買,彈出二維碼,掃一掃支付
掃一掃支付成功後,微信會回調我們的接口通知我們支付結果,根據如果微信通知我們該訂單支付成功,那我們就去更新該訂單的狀態爲已支付,然後前端頁面隔3秒Ajax異步去輪詢查詢訂單狀態,等查詢到的狀態是已支付,頁面會顯示支付成功
以上就是用SpringBoot搭建項目,完成了一個接入微信支付的功能