微信WAP H5支付功能實現

公司APP目前用到了微信的H5支付功能,這裏記錄一下實現過程。
這篇記錄可能回因爲微信商戶平臺的API的變動而變得不完全正確,但是大體流程時不會錯的。

1. 微信H5支付流程

不管我這文檔寫的多漂亮,咋們還是按照微信官方文檔來,下看流程圖,瞭解支付的大概過程:
在這裏插入圖片描述

一定要把這副圖看懂再去閱讀其他文檔就方便了。
簡單解釋一下就是:
用戶再瀏覽器下單—》傳回商家後臺—》商家後臺保存訂單,調用微信統一下單接口將訂單提交到微信支付後臺—》微信支付後臺返回一個“URL”給商戶後臺—》商戶後臺將URL以重定向的方式返給瀏覽器(其他方式也行,只要瀏覽器能獲取到)—》瀏覽器根據得到的URL拉起微信支付功能—》
支付完成後分成微信APP會做兩個事:
1.將支付結果給微信後臺的—》微信後臺確認支付,調用商戶後臺的回調接口—》商戶後臺確認支付完成,完成相關操作
2.返回到瀏覽器中—》瀏覽器去商戶後臺查詢是否支付完成—》將支付結果展示給用戶

2. 準備工作

  1. 在開放平臺註冊並認證APP信息
  2. 在微信商戶平臺完成賬號的註冊和商戶號的申請
  3. 將開放平臺和商戶平臺綁定
  4. 使用申請的商戶號登錄商戶號管理後臺,配置API密鑰;配置H5域名(下單網頁的域名必須和這個域名一致,否無法下單)

3. 後臺代碼

下單和回調接口

import com.alibaba.fastjson.JSONObject;
import plugins.pay.wechat.domain.WechatPayRet;
import plugins.pay.wechat.sdk.WXPayXmlUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

@RestController
@Api(tags="H5支付接口")
@RequestMapping("/H5Indent")
public class H5IndentController {

    @GetMapping("/addWechatH5")
    @ApiOperation(value = "微信H5新增訂單", notes = "微信H5新增訂單")
    public void addWechatH5(HttpServletRequest request, HttpServletResponse response){

        String APPID = "開放平臺APPID";
        String MERID = "商戶平臺商戶號";
        String SIGNKEY = "商戶平臺密鑰";
        String spbillCreateIp = getIpAddr(request);   // 用戶ip
        String scene_info = "{\"h5_info\": {\"type\":\"Wap\",\"wap_url\": \"https://www.xxx.com/pay/payCallback.html\"," +
                "\"wap_name\": \"開放平臺APP名字\"}}"; // 網頁回調地址,流程圖第6步所需的
        String tradeType = "MWEB";  // H5支付標記
        String MD5 = "MD5"; // 雖然官方文檔不是必須參數,但是不送有時候會驗籤失敗
        String subject = "會員充值";
        String totalFee = "支付金額";
        // 隨機字符串
        String nonce_str= getMessageDigest(String.valueOf(new Random().nextInt(10000)).getBytes());
        // 回調地址
        String notifyUrl = "商戶後臺回調接口地址";
        // 商戶訂單id
        String indentId = "xxxxxxxxxxx";

        //簽名數據
        StringBuilder sb = new StringBuilder()
                .append("appid=").append(APPID)
                .append("&body=").append(subject)
                .append("&mch_id=").append(MERID)
                .append("&nonce_str=").append(nonce_str)
                .append("&notify_url=").append(notifyUrl)
                .append("&out_trade_no=").append(indentId)
                .append("&scene_info=").append(scene_info)
                .append("&sign_type=").append(MD5)
                .append("&spbill_create_ip=").append(spbillCreateIp)
                .append("&total_fee=").append(totalFee)
                .append("&trade_type=MWEB")
                .append("&key=").append(SIGNKEY);
        //簽名MD5加密
        String sign = (md5(sb.toString())).toUpperCase(); //"把sb.toString()做MD5操作並且toUpperCase()一下,至於怎麼MD5,百度一下或者看官方文檔"

        //封裝xml報文
        String xml="<xml>"+
                "<appid>"+ APPID+"</appid>"+
                "<mch_id>"+ MERID+"</mch_id>"+
                "<nonce_str>"+nonce_str+"</nonce_str>"+
                "<sign>"+sign+"</sign>"+
                "<body>"+subject+"</body>"+//
                "<out_trade_no>"+indentId+"</out_trade_no>"+
                "<total_fee>"+totalFee+"</total_fee>"+//
                "<trade_type>"+tradeType+"</trade_type>"+
                "<notify_url>"+notifyUrl+"</notify_url>"+
                "<sign_type>MD5</sign_type>"+
                "<scene_info>"+scene_info+"</scene_info>"+
                "<spbill_create_ip>"+spbillCreateIp+"</spbill_create_ip>"+
                "</xml>";

        String createOrderURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";//微信統一下單接口
        String mweb_url = "";   // 微信後臺返回的URL

        Map map = new HashMap();
        JSONObject result = new JSONObject();
        try {
            // 下單,取接口地址
            map = getMwebUrl(createOrderURL, xml);
            String return_code  = (String) map.get("return_code");
            String return_msg = (String) map.get("return_msg");
            System.out.println(map);
            if("SUCCESS".equals(return_code) && "OK".equals(return_msg)){
                mweb_url = (String) map.get("mweb_url"); // 調微信支付接口地址
            }else{
                System.out.println("統一支付接口獲取預支付訂單出錯");
            }
        } catch (Exception e) {
            System.out.println("統一支付接口獲取預支付訂單出錯");
        }

		// TODO  商戶後臺保存訂單信息
        // 重定向返給前端跳轉URL
        response.addHeader("location", mweb_url + "&redirect_url=http%3A%2F%2Fwww.xxx.com/pay/payCallback.html");
        response.setStatus(302);
    }

    // 微信APP收款回調
    @PostMapping("/wechatNotify")
    public void wechatNotify(HttpServletRequest request, HttpServletResponse response)throws Exception{

        //解析數據
        WechatPayRet ret = parseRequest(request);

        String return_code = ret.getReturn_code();

        if(return_code.equals("SUCCESS")){
            // 開始進行訂單處理,ret.getOut_trade_no()就是商戶訂單號,delIndent()是業務處理邏輯自己完善
            // TODO  處理業務邏輯
            String result = delIndent(ret.getOut_trade_no());

            System.out.println("----------------------------------------------------------------------交易完成!");

            if(result.equals("success")){
                //成功之後要應答,讓微信別調了。但是還是會有重入的可能,所以必須做好數據鎖
                echo(response);
            }else{
                return;
            }
        }else{
            return;
        }
    }

    // 應答微信回調
    public static void echo(HttpServletResponse response) throws Exception {
        response.setContentType("application/xml");
        ServletOutputStream os = response.getOutputStream();
        os.print("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
    }

    // 解析微信後臺返回的數據
    public static WechatPayRet parseRequest(HttpServletRequest request) throws Exception {
        String xml = readXmlFromRequest(request);
        Map map = xmlToMap(xml);
        WechatPayRet ret = new WechatPayRet();
        ret.setReturn_code((String) map.get("result_code"));
        ret.setOut_trade_no((String) map.get("out_trade_no"));
        return ret;
    }

    // XML格式字符串轉換爲Map
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilder documentBuilder = WXPayXmlUtil.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) {
            getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            throw ex;
        }

    }

    // 日誌
    public static Logger getLogger() {
        Logger logger = LoggerFactory.getLogger("wxpay java sdk");
        return logger;
    }

    // 從request讀取xml
    public static String readXmlFromRequest(HttpServletRequest request) {
        StringBuilder xmlSb = new StringBuilder();
        try(
                ServletInputStream in = request.getInputStream();
                InputStreamReader inputStream = new InputStreamReader(in);
                BufferedReader buffer = new BufferedReader(inputStream);
        ){
            String line = null;
            while((line=buffer.readLine())!=null){
                xmlSb.append(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return xmlSb.toString();
    }

    // 生成隨機字符串
    public static String getMessageDigest(byte[] buffer) {
        char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
        try {
            MessageDigest mdTemp = MessageDigest.getInstance("MD5");
            mdTemp.update(buffer);
            byte[] md = mdTemp.digest();
            int j = md.length;
            char str[] = new char[j * 2];
            int k = 0;
            for (int i = 0; i < j; i++) {
                byte byte0 = md[i];
                str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(str);
        } catch (Exception e) {
            return null;
        }
    }

    // 獲取用戶ip
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = request.getHeader("x-forwarded-for");
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if(ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")){
                //根據網卡取本機配置的IP
                InetAddress inet=null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ipAddress= inet.getHostAddress();
            }
        }
        //對於通過多個代理的情況,第一個IP爲客戶端真實IP,多個IP按照','分割
        if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15
            if(ipAddress.indexOf(",")>0){
                ipAddress = ipAddress.substring(0,ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }

    // MD5加密
    public static String md5(String key) {
        System.out.println(key);
        char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        try {
            byte[] btInput = key.getBytes("UTF-8");
            // 獲得MD5摘要算法的 MessageDigest 對象
            MessageDigest mdInst = MessageDigest.getInstance("MD5");
            // 使用指定的字節更新摘要
            mdInst.update(btInput);
            // 獲得密文
            byte[] md = mdInst.digest();
            // 把密文轉換成十六進制的字符串形式
            int j = md.length;
            char str[] = new char[j * 2];
            int k = 0;
            for (int i = 0; i < j; i++) {
                byte byte0 = md[i];
                str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            String s = new String(str);
            s = s.toLowerCase();
            return s;
        } catch (Exception e) {
            return null;
        }
    }

    // 發送請求
    public static Map getMwebUrl(String url, String xmlParam){
        String jsonStr = null;
        HttpClient httpClient = new HttpClient();
        Map map = new HashMap();
        try {
            PostMethod method = null;
            RequestEntity reqEntity = new StringRequestEntity(xmlParam,"text/json","UTF-8");
            method = new PostMethod(url);
            method.setRequestEntity(reqEntity);
            method.addRequestHeader("Content-Type","application/json;charset=utf-8");
            httpClient.executeMethod(method);
            StringBuffer resBodyBuf = new StringBuffer();
            byte[] responseBody = new byte[1024];
            int readCount = 0;
            BufferedInputStream is = new BufferedInputStream(method.getResponseBodyAsStream());
            while((readCount = is.read(responseBody,0,responseBody.length))!=-1){
                resBodyBuf.append(new String(responseBody,0,readCount,"utf-8"));
            }
            jsonStr = resBodyBuf.toString();
            System.out.println(jsonStr);
            map = parseXmlToList(jsonStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }

    // 將xml轉成list
    public static Map parseXmlToList(String xml) {
        Map retMap = new HashMap();
        try {
            StringReader read = new StringReader(xml);
            // 創建新的輸入源SAX 解析器將使用 InputSource 對象來確定如何讀取 XML 輸入
            InputSource source = new InputSource(read);
            // 創建一個新的SAXBuilder
            SAXBuilder sb = new SAXBuilder();
            // 通過輸入源構造一個Document
            org.jdom.Document doc = sb.build(source);
            org.jdom.Element root = doc.getRootElement();// 指向根節點
            List<Element> es = root.getChildren();
            if (es != null && es.size() != 0) {
                for (org.jdom.Element element : es) {
                    retMap.put(element.getName(), element.getValue());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return retMap;

    }
}

微信後臺返回對象封裝,自行補充Getter和Setter

import com.alibaba.fastjson.annotation.JSONField;

import java.util.Date;

/**
 * 微信支付返回信息類
 * @date 2018年1月24日
 */
public class WechatPayRet {

	//返回狀態碼
	private String return_code;
	//返回信息
	private String return_msg;
	//應用ID
	private String appid;
	//商戶號
	private String mch_id;
	//設備號
	private String device_info;
	//隨機字符串
	private String nonce_str;
	//簽名
	private String sign;
	//業務結果
	private String result_code;
	//錯誤代碼
	private String err_code;
	//錯誤代碼描述
	private String err_des;
	//用戶標識
	private String openid;
	//是否關注公衆賬號
	private String is_subscribe;
	//交易類型
	private String trade_type;
	//付款銀行
	private String bank_type;
	//總金額
	private int total_fee;
	//貨幣種類
	private String fee_type;
	//現金支付金額
	private int cash_fee;
	//現金支付貨幣類型
	private String cash_fee_type;
	//代金券金額
	private int coupon_fee;
	//代金券使用數量
	private int coupon_count;
	//微信支付訂單號
	private String transaction_id;
	//商戶訂單號
	private String out_trade_no;
	//商家數據包
	private String attach;
	//支付完成時間
	@JSONField(format="yyyy-MM-dd HH:mm:ss")
	private Date time_end;
	
	/**
	 * 連接是否成功
	 * @return
	 */
	public boolean isContact(){
		return "SUCCESS".equals(this.return_code);
	}
	
	/**
	 * 業務是否成功
	 * @return
	 */
	public boolean isSuccess(){
		if(isContact()){
			return "SUCCESS".equals(this.result_code);
		}
		return false;
	}
}

XML工具類

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * 2018/7/3
 */
public final class WXPayXmlUtil {
    public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        documentBuilderFactory.setXIncludeAware(false);
        documentBuilderFactory.setExpandEntityReferences(false);

        return documentBuilderFactory.newDocumentBuilder();
    }

    public static Document newDocument() throws ParserConfigurationException {
        return newDocumentBuilder().newDocument();
    }
}

4. 其他文檔資料

可能遇到的問題,這些大佬都給出瞭解決辦法
https://www.cnblogs.com/lizhilin2016/p/9001452.html
https://blog.csdn.net/lql15005223252/article/details/83146412
https://blog.csdn.net/u010420435/article/details/79307125

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章