微信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

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