微信H5支付(Java)

一、场景介绍

微信H5支付是在手机移动浏览器端调起微信支付的方式。本文中仅介绍后台开发端的API对接,具体怎么开通H5支付,微信商户平台相关的内容请参考微信开发文档。

开通微信H5支付后,获取到APPID,商户号mch_id,商户支付**key等备用。

二、开发准备

1、域名

要求商户已有H5商城网站,并且域名已经过ICP备案。
所以,对于个人开发demo测试并不适合

2、项目

本文中一律使用SpringBoot项目下的配置。

3、配置文件

创建配置文件:api.properties,其中关键支付参数数据:

# APPID
wap.appid=wx1803e3r3f31614a6
# 商户号
wap.mchid=1494336672
# 支付**
wap.key=gOxGoTq6aYlg2ZFWvB2uhAR4Xh81l9U9
# 微信H5支付API地址
wap.unifiedorder=https://api.mch.weixin.qq.com/pay/unifiedorder

自定义配置文件的类,读取配置文件

@ConfigurationProperties(prefix = "wap", locations = "classpath:api.properties")
@Component
public class ApiConfig {
	private String appid;
	private String mchid;
	private String key;
	private String unifiedorder;
	public String getUnifiedorder() {
		return unifiedorder;
	}
	public void setUnifiedorder(String unifiedorder) {
		this.unifiedorder = unifiedorder;
	}
	public String getAppid() {
		return appid;
	}
	public void setAppid(String appid) {
		this.appid = appid;
	}
	public String getMchid() {
		return mchid;
	}
	public void setMchid(String mchid) {
		this.mchid = mchid;
	}
	public String getKey() {
		return key;
	}
	public void setKey(String key) {
		this.key = key;
	}
}

三、准备开发

1、订单创建

支付的必要条件是必须创建完订单,获取到订单的一系列数据,包括商户订单号,商品名称,商品介绍等。(每个商城都有自己的订单创建方式,此处不做详细介绍)

2、获取用户真实IP

由于安全性考虑,H5支付要求商户在统一下单接口中上传用户真实ip地址“spbill_create_ip”,保证微信端获取的用户ip地址与商户端获取的一致。

此处参考:微信支付开发文档
或者我的博文:穿透代理获取用户真实IP地址

3、API对接

首先是一个Controller方法:

    @ResponseBody
	@RequestMapping("/wechatPay")
	public String wechatPay(HttpServletRequest request, JSONObject order) {
		JSONObject ret = new JSONObject();
		ret.put("success", false);
		ret.put("msg", "请求失败[CCO01]");
		try {
			// 获取用户真实IP
			String ip = getClientIpAddress(request);
			// 微信API调用相关
			JSONObject wxPayJson = wxInfoService.getWXPayJSON(order,ip);
			logger.info("微信支付返回参数 "+wxPayJson);
			if("success".equalsIgnoreCase(wxPayJson.getString("return_code"))){
				if("success".equalsIgnoreCase(wxPayJson.getString("result_code"))){
					// 保存支付信息什么的
					insertOrderPadPay(order, wxPayJson.getString("prepay_id"));
					ret.put("success", true);
					ret.put("msg", "ok");
					ret.put("data", wxPayJson);
					ret.put("orderNO", order.getString("orderNo"));
				}
				if("fail".equalsIgnoreCase(wxPayJson.getString("result_code"))){
					ret.put("success", false);
					ret.put("msg", wxPayJson.getString("err_code_des"));
					ret.put("data", wxPayJson);
					ret.put("orderNO", order.getString("orderNo"));
				}
			}else {
				ret.put("success", false);
				ret.put("msg", wxPayJson.getString("err_code_des"));
				ret.put("data", wxPayJson);
				ret.put("orderNO", order.getString("orderNo"));
			}
		} catch (Exception e) {
			e.printStackTrace();
			ret.put("msg", e.getMessage());
		}
		return ret.toString();
	}

其中的获取用户IP的方法是:

private static final String[] HEADERS_TO_TRY = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
			"HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED", "HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP",
			"HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR", "PROXY_FORWARDED_FOR", "X-Real-IP"};

	/**
	 * getClientIpAddress:(获取用户ip,可穿透代理). 
	 * @author SongYapeng
	 * @Date 2018年3月2日下午4:41:47
	 * @param request
	 * @since JDK 1.8
	 */
	public static String getClientIpAddress(HttpServletRequest request) {
		for (String header : HEADERS_TO_TRY) {
			String ip = request.getHeader(header);
			if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
				if (ip != null && ip.indexOf(",") != -1) {
					String[] ips = ip.split(",");
					for (int i = 0; i < ips.length; i++) {
						String ipMulti = (String) ips[i];
						if (!("unknown".equalsIgnoreCase(ipMulti))) {
							ip = ipMulti;
							break;
						}
					}
				}
				return ip;
			}
		}
		return request.getRemoteAddr();
	}

接着getWXPayJSON方法实现如下:

public JSONObject getWXPayJSON(JSONObject order, String ip) throws Exception {
		Map<String, String> map = new HashMap<String, String>();
		/**
		 * order 商户订单信息
		 * appid 微信分配的公众账号ID(企业号corpid即为此appId)
		 * wap_url 网站域名
		 * nonce_str 随机字符串
		 * mch_id 微信支付分配的商户号
		 * notify_url 回调地址,支付结束后,根据相应的结果执行相应的步骤(如修改oms订单状态为已支付)
		 * out_trade_no 订单号
		 * total_fee 订单总金额,单位为分
		 * trade_type 支付方式微信h5支付
		 */
		String appid = apiConfig.getAppid();
		String wap_url = apiConfig.getWap_url();
		// 生成指定长度的随机字符串方法
		String nonce_str = CommonUtil.createNonceStr(10);
		// 商品描述
		String body = Constants.WAP_NAME + "订单号:" + order.getString("orderNo");
		String mch_id = apiConfig.getMchid(); 
		String notify_url = wap_url + "/notify.html";
		// 商户订单号
		String out_trade_no = order.getString("orderNo");
		String spbill_create_ip = ip;
		// 支付金额,单位 分
		Double saleMoney = order.getDouble("needSaleMoneySum") * 100;
		BigDecimal total_fee = new BigDecimal(saleMoney);
		total_fee = total_fee.setScale(0, BigDecimal.ROUND_HALF_UP);
		// 交易类型 微信H5支付
		String trade_type = "MWEB";
		JSONObject json = new JSONObject();
		// 场景信息
		JSONObject scene_info = new JSONObject();
		json.put("type", "WAP");
		json.put("wap_url", wap_url);
		// 网站名称,自定义
		json.put("wap_name", Constants.WAP_NAME);
		scene_info.put("h5_info", json);
		map.put("appid", appid);
		map.put("nonce_str", nonce_str);
		map.put("body", body);
		map.put("mch_id", mch_id);
		map.put("notify_url", notify_url);
		map.put("out_trade_no", out_trade_no);
		map.put("spbill_create_ip", spbill_create_ip);
		map.put("total_fee", total_fee + "");
		map.put("trade_type", trade_type);
		map.put("scene_info", scene_info.toString());
		// 签名,很重要
		map.put("sign", createSign(map, true));
		return getUnifiedorder(map);
	}

getWXPayJSON方法中的签名方法createSign(map, true)具体实现如下:

public String createSign(Map<String, String> map, boolean isLowerCase) throws Exception {
		// map取出空值
		Map<String, String> preMap = CommonUtil.delNull(map, isLowerCase);
		// 排序并把数组所有元素按照参数=参数名 的模式用&字符拼接成字符串
		String temp = CommonUtil.createSortParams(preMap, false, isLowerCase);
		//  拼上key=key(商户支付秘钥)进行md5运算,再将得到的字符串所有字符转换为大写
		String  signStr = temp + "&key=" + apiConfig.getKey();
		logger.info("待签名字符串:"+signStr);
		String sign = CommonUtil.Sign(temp, apiConfig.getKey());
		return sign;
	}

getWXPayJSON方法中的getUnifiedorder方法如下:

public JSONObject getUnifiedorder(Map<String, String> map) throws Exception {
		String unifiedorder = apiConfig.getUnifiedorder();
		String xml = CommonUtil.map2xml(map, false);
		String result = HttpClientUtil.httpPostXml(unifiedorder, null, xml);
		JSONObject json = CommonUtil.xml2JSON(result);
		logger.info("支付请求参数:"+map+";支付返回参数" + json);
		return json;
	}

上面方法中多次用到CommonUtil工具类中的方法,现呈上CommonUtil工具类:

package net.shopin.wap.common.util;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class CommonUtil {
    /**
     * 指定长度uuid
     * @param length 长度
     * @return String
     */
    public static String createUUID(int length) {
        if (length > 36) {
            throw new RuntimeException("请控制长度在36位以内!");
        } else {
            String[] chars = new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
                    "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8",
                    "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
                    "T", "U", "V", "W", "X", "Y", "Z"};
            StringBuffer shortBuffer = new StringBuffer();
            String uuid = UUID.randomUUID().toString().replace("-", "");
            for (int i = 0; i < length; i++) {
                String str = uuid.substring(i * 4, i * 4 + 4);
                int x = Integer.parseInt(str, 16);
                shortBuffer.append(chars[x % 0x3E]);
            }
            return shortBuffer.toString().toLowerCase();
        }
    }
    /**
     * 生成指定长度的随机数字
     * @param length 长度
     * @return String
     */
    public static String createNonceNum(int length) {
        String chars = "0123456789";
        String res = "";
        for (int i = 0; i < length; i++) {
            Random rd = new Random();
            res += chars.charAt(rd.nextInt(chars.length() - 1));
        }
        return res;
    }
    /**
     * 生成指定长度的随机字符串
     * @param length 长度
     * @return String
     */
    public static String createNonceStr(int length) {
        String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        String res = "";
        for (int i = 0; i < length; i++) {
            Random rd = new Random();
            res += chars.charAt(rd.nextInt(chars.length() - 1));
        }
        return res;
    }
    /**
     * xml2JSON:(xml转为fastJson).
     * @param xml
     * @return
     * @throws DocumentException
     * @author SongYapeng
     * @Date 2017年12月19日下午2:13:16
     * @since JDK 1.7
     */
    public static JSONObject xml2JSON(String xml) throws DocumentException {
        return elementToJSONObject(strToDocument(xml).getRootElement());
    }
    public static Document strToDocument(String xml) throws DocumentException {
        return DocumentHelper.parseText(xml);
    }
    public static JSONObject elementToJSONObject(Element node) {
        JSONObject result = new JSONObject();
        /**
         * 当前节点的名称、文本内容和属性
         * 当前节点的所有属性的list
         */
        @SuppressWarnings("unchecked")
        List<Attribute> listAttr = node.attributes();
        for (Attribute attr : listAttr) {
            result.put(attr.getName(), attr.getValue());
        }
        /**
         * 递归遍历当前节点所有的子节点
         * 所有一级子节点的list
         */
        @SuppressWarnings("unchecked")
        List<Element> listElement = node.elements();
        if (!listElement.isEmpty()) {
            /**
             * 遍历所有一级子节点
             */
            for (Element e : listElement) {
                /**
                 * 判断一级节点是否有属性和子节点
                 * 沒有则将当前节点作为上级节点的属性对待
                 */
                if (e.attributes().isEmpty() && e.elements().isEmpty())
                    result.put(e.getName(), e.getTextTrim());
                else {
                    /**
                     * 判断父节点是否存在该一级节点名称的属性
                     * 没有则创建
                     * 将该一级节点放入该节点名称的属性对应的值中
                     */
                    if (!result.containsKey(e.getName()))
                        result.put(e.getName(), new JSONArray());
                    ((JSONArray) result.get(e.getName())).add(elementToJSONObject(e));
                }
            }
        }
        return result;
    }
    /**
     * Map 转 XML
     * @param map
     * @param isLowerCase
     * @return
     */
    public static String map2xml(Map<String, String> map, boolean isLowerCase) {
        map = CommonUtil.delNull(map, isLowerCase);
        /**
         * 开始对map进行解析
         */
        if (map == null)
            throw new NullPointerException("map 数据为空,不能解析!");
        Document document = DocumentHelper.createDocument();
        Element nodeElement = document.addElement("xml");
        for (Object obj : map.keySet()) {
            Element keyElement = nodeElement.addElement(String.valueOf(obj));
            keyElement.setText(String.valueOf(map.get(obj)));
        }
        return doc2String(document);
    }
    /**
     * Document 转 String
     * @param document
     * @return String
     */
    public static String doc2String(Document document) {
        String s = "";
        try {
            /**
             * 使用输出流来进行转化
             */
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            OutputFormat format = new OutputFormat("   ", true, "UTF-8");
            XMLWriter writer = new XMLWriter(out, format);
            writer.write(document);
            s = out.toString("UTF-8");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return s;
    }
    /**
     * 去除Map中的空值
     * @param map
     * @return 去掉空值后的map
     */
    public static Map<String, String> delNull(Map<String, String> map, boolean isLowerCase) {

        Map<String, String> result = new HashMap<String, String>();

        if (map == null || map.size() <= 0) {
            return result;
        }

        for (String key : map.keySet()) {
            String value = map.get(key);
            if (value == null || value.equals("") || value.equals("null")) {
                continue;
            }
            if (isLowerCase) {
                result.put(key.toLowerCase(), value);
            } else {
                result.put(key, value);
            }
        }

        return result;
    }
    /**
     * 把Map所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     * @param params      需要排序并参与字符拼接的Map
     * @param isEncode    是否对value进行urlencode
     * @param isLowerCase 是否转换小写
     * @return 拼接后字符串
     */
    public static String createSortParams(Map<String, String> params, boolean isEncode, boolean isLowerCase) {
        String result = "";
        try {
            List<String> keys = new ArrayList<String>(params.keySet());
            Collections.sort(keys);
            if (isEncode) {
                for (int i = 0; i < keys.size(); i++) {
                    String key = keys.get(i);
                    if (isLowerCase) {
                        key = key.toLowerCase();
                    }
                    String value = URLEncoder.encode(params.get(key), "UTF-8");
                    result = result + key + "=" + value + "&";
                }
            } else {
                for (int i = 0; i < keys.size(); i++) {
                    String key = keys.get(i);
                    if (isLowerCase) {
                        key = key.toLowerCase();
                    }
                    String value = params.get(key);
                    result = result + key + "=" + value + "&";
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result.substring(0, result.length() - 1);
    }
    /**
     * MD5签名,微信专用
     * @param content 内容
     * @param key     key值
     * @return
     */
    public static String Sign(String content, String key) throws Exception {
        String signStr = "";

        if ("" == key) {
            throw new Exception("财付通签名key不能为空!");
        }
        if ("" == content) {
            throw new Exception("财付通签名内容不能为空");
        }
        signStr = content + "&key=" + key;
        return MD5(signStr).toUpperCase();

    }
    /**
     * MD5 加密
     * @param data
     * @return
     */
    public final static String MD5(String data) {
        char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        try {
            byte[] btInput = data.getBytes();
            /**
             * 获得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];
            }
            return new String(str);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * sh1 加密
     * @param s
     * @return
     */
    public final static String Sha1(String s) {
        char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
        try {
            byte[] btInput = s.getBytes();
            /**
             * 获得MD5摘要算法的 MessageDigest 对象
             */
            MessageDigest mdInst = MessageDigest.getInstance("sha-1");
            /**
             * 使用指定的字节更新摘要
             */
            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];
            }
            return new String(str);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 将cookie封装到Map里面
     * @param request
     * @return
     */
    public static Map<String, Cookie> getCookieMap(HttpServletRequest request) {
        Map<String, Cookie> cookieMap = new HashMap<String, Cookie>();
        Cookie[] cookies = request.getCookies();
        if (null != cookies) {
            for (Cookie cookie : cookies) {
                cookieMap.put(cookie.getName(), cookie);
            }
        }
        return cookieMap;
    }
    /**
     * 保留两位小数的double
     * @param number
     * @return
     */
    public static String formatDouble(Double number) {
        return new DecimalFormat("######0.00").format(number);
    }
    public static String formatDouble1(Double number) {
        return new DecimalFormat("######0.0").format(number);
    }
    /**
     * 从request 中获取字符串
     */
    public static String getStringFrom(HttpServletRequest request) throws Exception {
        InputStream in = request.getInputStream();
        StringBuffer out = new StringBuffer();
        byte[] b = new byte[1024];
        for (int n; (n = in.read(b)) != -1; ) {
            out.append(new String(b, 0, n));
        }
        return out.toString();
    }
    /**
     * 随机生成 指定范围的小数 min :最小值范围 max:最大值范围
     */
    public static BigDecimal getDecimalNum(int min, int max) {
        Random random = new Random();
        int s = random.nextInt(max) % (max - min + 1) + min;
        String temp = "0." + s;
        BigDecimal number = new BigDecimal(temp);
        return number;
    }
}

通过以上代码请求,最终可获取到支付跳转链接:mweb_url,mweb_url为拉起微信支付收银台的中间页面,可通过访问该url来拉起微信客户端,完成支付,mweb_url的有效期为5分钟。

具体更多API参数请参考微信支付文档:微信H5支付文档

猜你喜欢