欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

微信H5支付(Java)

程序员文章站 2022-07-13 17:15:03
...

一、场景介绍

微信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支付文档