1. 程式人生 > >微信掃碼支付之Java遊記

微信掃碼支付之Java遊記

《滿江紅·雨後晴初》
年代: 宋 作者: 京鏜
雨後晴初,覺春在、榿村柳陌。修禊事、郊坰尋勝,特邀君出。繚繞群山疑虎踞,瀰漫一水容鯨吸。怪西湖、底事卻移來,龜城北。酬令節,逢佳日。風遞暖,煙凝碧。趁蘭舟遊玩,盡杯中物。十里輪蹄塵不斷,幾多粉黛花無色。笑杜陵、昔賦麗人行,空遺蹟。

喜歡雨後晴初的感覺,看網上說支付寶介面文件亂如初,微信支付介面亂如麻。

今天先研究微信支付(掃碼支付),快刀斬亂麻。

首先,先來看看微信掃碼支付API文件

業務流程說明:

(1)商戶後臺系統根據微信支付規定格式生成二維碼(規則見下文),展示給使用者掃碼。

(2)使用者開啟微信“掃一掃”掃描二維碼,微信客戶端將掃碼內容傳送到微信支付系統。

(3)微信支付系統收到客戶端請求,發起對商戶後臺系統支付回撥URL的呼叫。呼叫請求將帶productid和使用者的openid等引數,並要求商戶系統返回交資料包,詳細請見"本節3.1回撥資料輸入引數"

(4)商戶後臺系統收到微信支付系統的回撥請求,根據productid生成商戶系統的訂單。

(5)商戶系統呼叫微信支付【統一下單API】請求下單,獲取交易會話標識(prepay_id)

(6)微信支付系統根據商戶系統的請求生成預支付交易,並返回交易會話標識(prepay_id)。

(7)商戶後臺系統得到交易會話標識prepay_id(2小時內有效)。

(8)商戶後臺系統將prepay_id返回給微信支付系統。返回資料見"本節3.2回撥資料輸出引數"

(9)微信支付系統根據交易會話標識,發起使用者端授權支付流程。

(10)使用者在微信客戶端輸入密碼,確認支付後,微信客戶端提交支付授權。

(11)微信支付系統驗證後扣款,完成支付交易。

(12)微信支付系統完成支付交易後給微信客戶端返回交易結果,並將交易結果通過簡訊、微信訊息提示使用者。微信客戶端展示支付交易結果頁面。

(13)微信支付系統通過傳送非同步訊息通知商戶後臺系統支付結果。商戶後臺系統需回覆接收情況,通知微信後臺系統不再發送該單的支付通知。

(14)未收到支付通知的情況,商戶後臺系統呼叫【查詢訂單API】。

(15)商戶確認訂單已支付後給使用者發貨。

其實,我總結一下,上邊說對應的編碼流程:

步驟一:你需要打包微信所需的引數

1.1 輸入引數 

微信所需要的輸入引數說明

名稱變數名型別必填示例值描述
公眾賬號IDappidString(32)wx8888888888888888微信分配的公眾賬號ID
使用者標識openidString(128)o8GeHuLAsgefS_80exEr1cTqekUs使用者在商戶appid下的唯一標識
商戶號mch_idString(32)1900000109微信支付分配的商戶號
是否關注公眾賬號is_subscribeString(1)Y使用者是否關注公眾賬號,僅在公眾賬號型別支付有效,取值範圍:Y或N;Y-關注;N-未關注
隨機字串nonce_strString(32)5K8264ILTKCH16CQ2502SI8ZNMTM67VS隨機字串,不長於32位。推薦隨機數生成演算法
商品IDproduct_idString(32)88888商戶定義的商品id 或者訂單號
簽名signString(32)C380BEC2BFD727A4B6845133519F3AD6

步驟二:打包引數的時候扔給微信,返回微信一個code碼,根據code碼,你可以選擇google二維碼生成規則,或者其他二維碼生成器來生成

二維碼中的內容為連結,形式為:

weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX

其中XXXXX為商戶需要填寫的內容,商戶將該連結生成二維碼,如需要打印發布二維碼,需要採用此格式。商戶可呼叫第三方庫生成二維碼圖片。引數說明如下:

步驟三:使用者通過掃碼支付之後,會呼叫你配置檔案的回撥介面,主要是通過報文的形式來進行傳輸的

1.2 輸出引數

微信輸出引數說明

名稱變數名型別必填示例值描述
返回狀態碼return_codeString(16)SUCCESSSUCCESS/FAIL,此欄位是通訊標識,非交易標識,交易是否成功需要檢視result_code來判斷
返回資訊return_msgString(128)簽名失敗返回資訊,如非空,為錯誤原因;簽名失敗;具體某個引數格式校驗錯誤.
公眾賬號IDappidString(32)wx8888888888888888微信分配的公眾賬號ID
商戶號mch_idString(32)1900000109微信支付分配的商戶號
隨機字串nonce_strString(32)5K8264ILTKCH16CQ2502SI8ZNMTM67VS微信返回的隨機字串
預支付IDprepay_idString(64)wx201410272009395522657a690389285100呼叫統一下單介面生成的預支付ID
業務結果result_codeString(16)SUCCESSSUCCESS/FAIL
錯誤描述err_code_desString(128)當result_code為FAIL時,商戶展示給使用者的錯誤提
簽名signString(32)C380BEC2BFD727A4B6845133519F3AD6

上邊是一個流程介紹:

下面開始實戰,業務清楚了,程式碼就好實現了,也許會出現錯誤(坑),但是,身為程式設計師,天職就是以解決bug為快樂。

專案框架:springboot+spring security+jpa+jwt認證

application.yaml檔案

wx:
  pay:
    appId:********
    appSecret:*********
    mchId:********
    apiKey:*********ufdoderUrl: https://api.mch.weixin.qq.com/pay/unifiedorder
    notifyUrl: //回撥地址
    createIp:  
    signType: MD5

package com.***.service;

import com.***.domain.*;
import com.***.domain.repository.*;
import com.***.domain.request.RequestNotify;
import com.***.domain.request.RequestOrder;
import com.***.domain.sys.Result;
import com.***.enums.ResultEnum;
import com.***.enums.StateEnum;
import com.***.excpetion.ClassOnlineException;
import com.***.utils.CommonUtil;
import com.***.utils.HttpUtil;
import com.***.utils.ResultUtil;
import com.***.utils.XMLUtil;
import com.***.utils.wxpay.WXPayUtil;
import org.jdom.JDOMException;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mobile.device.Device;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.Transactional;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

@Service
public class WxPayServiceImpl implements WxPayService {


    @Value("${wx.pay.appId}")
    private String appId;

    @Value("${wx.pay.mchId}")
    private String mchId;

    @Value("${wx.pay.apiKey}")
    private String apiKey;


    @Value("${wx.pay.notifyUrl}")
    private String notifyUrl;

    @Value("${wx.pay.createIp}")
    private String createIp;

    @Value("${wx.pay.ufdoderUrl}")
    private String ufdoderUrl;

    private String charset = "utf-8";

    final String TRADETYPE = "NATIVE";
    final Long   MAXCLASSES = 30L;
    @Autowired
private OrdersRepository ordersRepository;

    @Autowired
private UserRepository userRepository;

    @Autowired
private CoursesRepository coursesRepository;

    @Autowired
private ClassesRepository classesRepository;

    @Autowired
private CourseRecordRepository courseRecordRepository;

    private static org.slf4j.Logger logger = LoggerFactory.getLogger(WxPayServiceImpl.class)
    @Override
public String wxPay(RequestOrder requestOrder, HttpServletRequest httpServletRequest, Device device) throws Exception {
        Orders orders =  ordersRepository.findByOrderNumber(requestOrder.getOrderNumber());
        if(orders == null)
            throw new ClassOnlineException(ResultEnum.NOT_FIND);
        //交易號(訂單號)
String outTradeNo =  orders.getOrderNumber(); 
        String totalAmount = CommonUtil.subZeroAndDot(Float.toString(orders.getPrice())); //注意這個地方,微信是一分為單位,不支援小數點
        String subject = orders.getCourse().getName();
        String body = orders.getUser().getUsername() + "購入" + orders.getClassName();

        SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
        packageParams.put("appid", appId);
        packageParams.put("mch_id", mchId);
        packageParams.put("nonce_str", CommonUtil.uniqueUUID());
        packageParams.put("body", body);
        packageParams.put("out_trade_no", outTradeNo);
        packageParams.put("total_fee", totalAmount);
        packageParams.put("spbill_create_ip", CommonUtil.getIpAddr(httpServletRequest));
        packageParams.put("notify_url", notifyUrl);
        packageParams.put("trade_type", TRADETYPE);

        //建立一個簽名
String sign = WXPayUtil.createSign("UTF-8", packageParams,apiKey);
        packageParams.put("sign", sign);
        //獲取微信所需的引數包,上面所需的一個不少,少了會出現引數錯誤
String requestXML = XMLUtil.getRequestXml(packageParams);
        System.out.println(requestXML);

        //通過微信統一呼叫微信介面https://api.mch.weixin.qq.com/pay/unifiedorder來獲取請求
String resXml = HttpUtil.postData(ufdoderUrl, requestXML);

        Map map = null;
        try {
            map = XMLUtil.doXMLParse(resXml);
            String urlCode = (String) map.get("code_url");
            String qrUrlCode = CommonUtil.QRfromGoogle(urlCode);
            return qrUrlCode;
        } catch (JDOMException e) {
            throw new ClassOnlineException(-1,e.getMessage());
        } catch (IOException e) {
            throw new ClassOnlineException(-1,e.getMessage());
        }
    }


    //微信回撥程式碼
    @Transactional
public Result<?> wxPayNotify(HttpServletRequest request, HttpServletResponse response) throws Exception{

        //讀取引數
InputStream inputStream ;
        StringBuffer sb = new StringBuffer();
        inputStream = request.getInputStream();
        String s ;
        BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        while ((s = in.readLine()) != null){
            sb.append(s);
        }
        in.close();
        inputStream.close();

        //解析xml成map
Map<String, String> m = new HashMap<String, String>();
        m = XMLUtil.doXMLParse(sb.toString());

        //過濾空 設定 TreeMap
SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
        Iterator it = m.keySet().iterator();
        while (it.hasNext()) {
            String parameter = (String) it.next();
            String parameterValue = m.get(parameter);

            String v = "";
            if(null != parameterValue) {
                v = parameterValue.trim();
            }
            packageParams.put(parameter, v);
        }
        // 賬號資訊
String key = apiKey; // key
logger.info(String.valueOf(packageParams));
        //判斷簽名是否正確
if(WXPayUtil.isTenpaySign("UTF-8", packageParams,key)) {
            //------------------------------
            /**
             * 處理業務             * 一整套流程用事務來控制
             */
String resXml = "";
            String resultCode = (String) packageParams.get("result_code");
            if("SUCCESS".equals(resultCode)){
                // 這裡是支付成功執行自己的業務邏輯
String mch_id = (String)packageParams.get("mch_id");
                String openid = (String)packageParams.get("openid");
                String is_subscribe = (String)packageParams.get("is_subscribe");
                String out_trade_no = (String)packageParams.get("out_trade_no");
                String total_fee = (String)packageParams.get("total_fee");

                RequestNotify requestNotify = new RequestNotify();
                requestNotify.setOrderNumber(out_trade_no);
                requestNotify.setPaymentType(StateEnum.ONLINE_WX.getCode());
                //呼叫支付成功後的業務程式碼
                ******************省略,每個公司不一樣
                logger.info("支付成功");
                //通知微信.非同步確認成功.必寫.不然會一直通知後臺.八次之後就認為交易失敗了.
resXml = returnXML(resultCode);

            } else {
                logger.info("支付失敗,錯誤資訊:" + packageParams.get("err_code"));
                resXml = returnXML("FAIL");
                throw new ClassOnlineException(ResultEnum.ORDER_PAY_FAIL);
            }
            //------------------------------
            //處理業務完畢
            //------------------------------
BufferedOutputStream out = new BufferedOutputStream(
                    response.getOutputStream());
            out.write(resXml.getBytes());
            out.flush();
            out.close();
        } else{
            logger.info("通知簽名驗證失敗");
            throw new ClassOnlineException(ResultEnum.SIGN_FAIL);
        }
        return  ResultUtil.success(ResultEnum.SUCCESS);
    }



    private String returnXML(String return_code) {

        return "<xml><return_code><![CDATA["
+ return_code

                + "]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
    }
}

下面是工具類:

package com.***.utils;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;

public class CommonUtil {

    //google 二維碼生成器
public static String QRfromGoogle(String chl) throws Exception {
        int widhtHeight = 300;
        String EC_level = "L";
        int margin = 0;
        chl = UrlEncode(chl);
        String QRfromGoogle = "http://chart.apis.google.com/chart?chs=" + widhtHeight + "x" + widhtHeight
                + "&cht=qr&chld=" + EC_level + "|" + margin + "&chl=" + chl;

        return QRfromGoogle;
    }


    // 特殊字元處理
public static String UrlEncode(String src)  throws UnsupportedEncodingException {
        return URLEncoder.encode(src, "UTF-8").replace("+", "%20");
    }

    //生成唯一碼
public static String uniqueUUID(){
        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
    }

    //獲取真實IP地址
public static  String getIpAddr(HttpServletRequest request) {
        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();
        }
        return ip;
    }

    /**
     * 去掉float小數點後邊的數字
     * @param s
* @return
*/
public static String subZeroAndDot(String s){
        s=s.substring(0, s.indexOf('.'));
        return s;
    }

    public static String getOrderIdByUUId() {
        int machineId = 1;//最大支援1-9個叢集機器部署
int hashCodeV = UUID.randomUUID().toString().hashCode();
        if(hashCodeV < 0) {//有可能是負數
hashCodeV = - hashCodeV;
        }
        // 0 代表前面補充0
        // 4 代表長度為4
        // d 代表引數為正數型
return machineId + String.format("%015d", hashCodeV);
    }

    //獲取日期點凌晨和二十四時
public static Date getDate(Date date, int flag) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        int hour = cal.get(Calendar.HOUR_OF_DAY);
        int minute = cal.get(Calendar.MINUTE);
        int second = cal.get(Calendar.SECOND);
        //時分秒(毫秒數)
long millisecond = hour*60*60*1000 + minute*60*1000 + second*1000;
        //凌晨00:00:00
cal.setTimeInMillis(cal.getTimeInMillis()-millisecond);

        if (flag == 0) {
            return cal.getTime();
        } else if (flag == 1) {
            //凌晨23:59:59
cal.setTimeInMillis(cal.getTimeInMillis()+23*60*60*1000 + 59*60*1000 + 59*1000);
        }
        return cal.getTime();
    }

}
接下來就是:微信的建立簽名,解析簽名處理的工具類都是以xml的形式進行傳遞的(你可以用它原生的V3,也可以自己寫)
package com.***.utils.wxpay;

import com.***.utils.MD5Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
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.security.MessageDigest;
import java.util.*;



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) {
            WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            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 的 XML 格式字串
     *
     * @param data Map型別資料
     * @param key API金鑰
     * @return 含有sign欄位的XML
     */
public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
        return generateSignedXml(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 生成帶有 sign 的 XML 格式字串
     *
     * @param data Map型別資料
     * @param key API金鑰
     * @param signType 簽名型別
     * @return 含有sign欄位的XML
     */
public static String generateSignedXml(final Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
        String sign = generateSignature(data, key, signType);
        data.put(WXPayConstants.FIELD_SIGN, sign);
        return mapToXml(data);
    }


    /**
     * 判斷簽名是否正確
     *
     * @param xmlStr XML格式資料
     * @param key API金鑰
     * @return 簽名是否正確
     * @throws Exception
     */
public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
        Map<String, String> data = xmlToMap(xmlStr);
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key).equals(sign);
    }

    /**
     * 判斷簽名是否正確,必須包含sign欄位,否則返回false。使用MD5簽名。
     *
     * @param data Map型別資料
     * @param key API金鑰
     * @return 簽名是否正確
     * @throws Exception
     */
public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
        return isSignatureValid(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 判斷簽名是否正確,必須包含sign欄位,否則返回false。
     *
     * @param data Map型別資料
     * @param key API金鑰
     * @param signType 簽名方式
     * @return 簽名是否正確
     * @throws Exception
     */
public static boolean isSignatureValid(Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
        if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
            return false;
        }
        String sign = data.get(WXPayConstants.FIELD_SIGN);
        return generateSignature(data, key, signType).equals(sign);
    }

    /**
     * 生成簽名
     *
     * @param data 待簽名資料
     * @param key API金鑰
     * @return 簽名
     */
public static String generateSignature(final Map<String, String> data, String key) throws Exception {
        return generateSignature(data, key, WXPayConstants.SignType.MD5);
    }

    /**
     * 生成簽名. 注意,若含有sign_type欄位,必須和signType引數保持一致。
     *
     * @param