实现微信支付

Java 实现JSAPI 微信支付

开发步骤如下:

1. 参考微信开发文档

把微信支付需要配置的环境都配好,参考链接 微信开发文档

2. 微信统一下单

参考链接 统一下单

import com.alibaba.fastjson.JSONObject;
import com.wholesale.mall.entity.WeiChart;
import com.wholesale.mall.mapper.systemSet.WeiChartMapper;
import com.wholesale.mall.service.statement.IPaymentService;
import com.wholesale.mall.utils.Tools;
import com.wholesale.mall.weichart.Sha1Util;
import com.wholesale.mall.weichart.WeiChatURLUtil;
import lombok.RequiredArgsConstructor;
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.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

	/**微信支付**/
    private final String wx_partner="1277054588549562368";
    private final String wx_pertnerKey="FFFF9BB656EE4430A31B356726BBC133";

    private final WeiChartMapper weiChartMapper;
    
	/**
     * 微信支付
     * @param request
     * @param response
     * @param orderNo 订单号
     * @param totalMoney 订单金额
     * @param openId 微信唯一标识
     * @return
     * @throws Exception
     */
    @Override
    public Map<Integer,Map<String,String>> saveWXPay(HttpServletRequest request, HttpServletResponse response,String orderNo, Double totalMoney, String openId) throws Exception{
        //返回参数
        Map<Integer,Map<String,String>> map=new HashMap<>();
        Map<String,String> mapVal=new HashMap<>();
        //金额转化为分为单位
        String money=(totalMoney*100)+"";//转换
        Float sessionmoney =Float.parseFloat(money);
        String finalmoney=sessionmoney.toString();
        finalmoney = finalmoney.replace(".0", "");

        //获取openId后调用统一支付接口https://api.mch.weixin.qq.com/pay/unifiedorder
        //商户号
        String mch_id = wx_partner;
        //10位序列号,可以自行调整。
        String strReq = Tools.CreateNoncestr();
        //随机数
        String nonce_str = strReq;
        //商品描述
        String body = "商品购买";
        //附加数据
        String attach = "商品购买";
        //商户订单号   商家内部订单号
        String out_trade_no = orderNo;
        //总金额以分为单位,不带小数点
        String total_fee = finalmoney;
        //String total_fee = "1";
        //订单生成的机器 IP
        String spbill_create_ip = request.getRemoteAddr();
        //这里notify_url是 支付完成后微信发给该链接信息,可以判断会员是否支付成功,改变订单状态等。
        String notify_url = WeiChatURLUtil.WEICHAT_BACKSTAGE_URL + "shipping-MinPay/updatePaymentState";
        //交易类型
        String trade_type = "JSAPI";

        //查询微信公众号信息
        WeiChart weiChart=weiChartMapper.findWeiChart();
        SortedMap<String, String> packageParams = new TreeMap<String, String>();
        packageParams.put("appid", weiChart.getAppId());
        packageParams.put("mch_id", mch_id);
        packageParams.put("nonce_str", nonce_str);
        packageParams.put("attach", attach);
        packageParams.put("body", body);
        packageParams.put("notify_url", notify_url);
        packageParams.put("openid", openId);
        packageParams.put("out_trade_no", out_trade_no);
        packageParams.put("spbill_create_ip", spbill_create_ip);
        //这里写的金额为1 分到时修改
        packageParams.put("total_fee", total_fee);
        packageParams.put("trade_type", trade_type);

        //签名
        String sign = Tools.createSign2(packageParams,wx_pertnerKey);
        String xml = "<xml>" +
                "<appid>" + weiChart.getAppId() + "</appid>" +
                "<mch_id>" + mch_id + "</mch_id>" +
                "<nonce_str>" + nonce_str + "</nonce_str>" +
                "<sign>" + sign + "</sign>" +
                "<body><![CDATA[" + body + "]]></body>" +
                "<attach>" + attach + "</attach>" +
                "<out_trade_no>" + out_trade_no + "</out_trade_no>" +
                //金额,这里写的1 分到时修改
                "<total_fee>" + total_fee + "</total_fee>" +
                "<spbill_create_ip>" + spbill_create_ip + "</spbill_create_ip>" +
                "<notify_url>" + notify_url + "</notify_url>" +
                "<trade_type>" + trade_type + "</trade_type>" +
                "<openid>" + openId + "</openid>" +
                "</xml>";
        logger.info(xml);
        String allParameters = "";
        try {
            allParameters = Tools.genPackage(packageParams,wx_pertnerKey);
        } catch (Exception e) {
            logger.error("获取package的签名包异常:", e);
            mapVal.put("errorMsg","获取package的签名包异常:"+ e.getMessage());
            map.put(1,mapVal);
            return map;
        }
        logger.info("获取package的签名包:" + allParameters);
        String createOrderURL = WeiChatURLUtil.WEICHAT_CREATEORDER_URL;
        String prepay_id = "";
        try {
            prepay_id = Tools.getPayNo(createOrderURL, xml);
            if ("".equals(prepay_id)) {
                logger.info("统一支付接口获取预支付订单出错");
                mapVal.put("errorMsg","统一支付接口获取预支付订单出错");
                map.put(1,mapVal);
                return map;
            }
        } catch (Exception e1) {
            logger.error("统一支付接口获取预支付订单异常:", e1);
            mapVal.put("errorMsg","统一支付接口获取预支付订单出错:"+e1.getMessage());
            map.put(1,mapVal);
            return map;
        }
        SortedMap<String, String> finalpackage = new TreeMap<String, String>();
        String appid2 = weiChart.getAppId();
        String timestamp = Sha1Util.getTimeStamp();
        String nonceStr2 = nonce_str;
        String packages = "prepay_id=" + prepay_id;
        finalpackage.put("appId", appid2);
        finalpackage.put("timeStamp", timestamp);
        finalpackage.put("nonceStr", nonceStr2);
        finalpackage.put("package", packages);
        finalpackage.put("signType", "MD5");
        String finalSign = Tools.createSign2(finalpackage,wx_pertnerKey);
        logger.info("提交页面数据:appid=" + appid2 + "&timeStamp=" + timestamp + "&nonceStr=" + nonceStr2 + "&package=" + packages + "&sign=" + finalSign);
        mapVal.put("orderId", prepay_id);
        mapVal.put("appId",appid2);
        mapVal.put("timestamp",timestamp);
        mapVal.put("nonceStr",nonceStr2);
        mapVal.put("packages",packages);
        mapVal.put("signType","MD5");
        mapVal.put("sign",finalSign);
        map.put(0,mapVal);
        return map;
    }
说明:

1.appId和appSecret在微信公众号平台获取 微信公众平台
商户号和商户秘钥在微信支付商户后台获取 微信支付

2.随机字符串 nonce_str的长度要求在32位以内。
推荐随机数生成算法 :

	/**
	 * 生成随机数
	 * 
	 * @return
	 */
	public static String CreateNoncestr() {
		Random random = new Random();
		return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8");
	}


import java.security.MessageDigest;

	/**
	 * MD5算法计算
	 */
	public class MD5Util {
	private static String byteArrayToHexString(byte b[]) {
		StringBuffer resultSb = new StringBuffer();
		for (int i = 0; i < b.length; i++)
			resultSb.append(byteToHexString(b[i]));

		return resultSb.toString();
	}

	private static String byteToHexString(byte b) {
		int n = b;
		if (n < 0)
			n += 256;
		int d1 = n / 16;
		int d2 = n % 16;
		return hexDigits[d1] + hexDigits[d2];
	}

	public static String MD5Encode(String origin, String charsetname) {
		String resultString = null;
		try {
			resultString = new String(origin);
			MessageDigest md = MessageDigest.getInstance("MD5");
			if (charsetname == null || "".equals(charsetname))
				resultString = byteArrayToHexString(md.digest(resultString
						.getBytes()));
			else
				resultString = byteArrayToHexString(md.digest(resultString
						.getBytes(charsetname)));
		} catch (Exception exception) {
		}
		return resultString;
	}

	private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
			"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };

}

3.参数 total_fee,即订单总金额,单位为分,以元为单位的记得转换为分。

4.商户订单号 out_trade_no,商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|* 且在同一个商户号下唯一。即为下单的订单号即可。

5.终端IP spbill_create_ip,支持IPV4和IPV6两种格式的IP地址。用户的客户端IP。代码如下:

	//订单生成的机器 IP
    String spbill_create_ip = request.getRemoteAddr();

6.签名算法生成:
参考文档:签名算法如何生成
特别注意以下重要规则:
◆ 参数名ASCII码从小到大排序(字典序);
◆ 如果参数的值为空不参与签名;
◆ 参数名区分大小写;
◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段。

使用SortedMap进行有序排列:

	SortedMap<String, String> packageParams = new TreeMap<String, String>();
    packageParams.put("appid", weiChart.getAppId());
    packageParams.put("mch_id", mch_id);
    packageParams.put("nonce_str", nonce_str);
    packageParams.put("attach", attach);
    packageParams.put("body", body);
    packageParams.put("notify_url", notify_url);
    packageParams.put("openid", openId);
    packageParams.put("out_trade_no", out_trade_no);
    packageParams.put("spbill_create_ip", spbill_create_ip);
    //这里写的金额为1 分到时修改
    packageParams.put("total_fee", "1");
    packageParams.put("trade_type", trade_type);

创建签名,使用哈希算法:

	/**
     * 创建md5摘要,规则是:将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),
     * 使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串
     * @param packageParams 有序参数包
     * @param key 商户秘钥
     * @return
     */
    public static String createSign2(SortedMap<String, String> packageParams, String key) 			{
        StringBuffer sb = new StringBuffer();
        Set<Map.Entry<String, String>> es = packageParams.entrySet();
        Iterator<Map.Entry<String, String>> it = es.iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            String k = entry.getKey();
            Object v = entry.getValue();
            if (null != v && !"".equals(v) && !"sign".equals(k)
                    && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + key);
        String sign = MD5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
        return sign;
    }

最终得到最终发送的数据,以xml格式发送:
(注:参数值用XML转义即可,CDATA标签用于说明数据不被XML解析器解析。)

String xml = "<xml>" +
       "<appid>" + weiChart.getAppId() + "</appid>" +
       "<mch_id>" + mch_id + "</mch_id>" +
       "<nonce_str>" + nonce_str + "</nonce_str>" +
       "<sign>" + sign + "</sign>" +
       "<body><![CDATA[" + body + "]]></body>" +
       "<attach>" + attach + "</attach>" +
       "<out_trade_no>" + out_trade_no + "</out_trade_no>" +
       //金额,这里写的1 分到时修改
       "<total_fee>" + total_fee + "</total_fee>" +
       "<spbill_create_ip>" + spbill_create_ip + "</spbill_create_ip>" +
       "<notify_url>" + notify_url + "</notify_url>" +
       "<trade_type>" + trade_type + "</trade_type>" +
       "<openid>" + openId + "</openid>" +
       "</xml>";

获取签名包:

	/**
     * 获取package的签名包
     * @param packageParams 有序参数包
     * @param key 商户秘钥
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String genPackage(SortedMap<String, String> packageParams, String key)throws UnsupportedEncodingException {
        String sign = createSign2(packageParams,key);

        StringBuffer sb = new StringBuffer();
        Set<Map.Entry<String, String>> es = packageParams.entrySet();
        Iterator<Map.Entry<String, String>> it = es.iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            String k = entry.getKey();
            String v = entry.getValue();
            sb.append(k + "=" + UrlEncode(v) + "&");
        }

        // 去掉最后一个&
        String packageValue = sb.append("sign=" + sign).toString();
        return packageValue;
    }

7.发送post请求,返回成功后,获取预支付交易会话标识 prepay_id。


	private static Logger logger = Logger.getLogger(Tools.class);
    public static DefaultHttpClient httpclient;
    static {
        httpclient = new DefaultHttpClient();
        httpclient = (DefaultHttpClient) HttpClientConnectionManager.getSSLInstance(httpclient);
    }
	
	/**
     * 获取支付prepay_id
     *
     * @param url https://api.mch.weixin.qq.com/pay/unifiedorder
     * @param xmlParam
     * @return
     */
    public static String getPayNo(String url, String xmlParam) {
        DefaultHttpClient client = new DefaultHttpClient();
        client.getParams().setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS,
                true);
        HttpPost httpost = HttpClientConnectionManager.getPostMethod(url);
        String prepay_id = "";
        try {
            httpost.setEntity(new StringEntity(xmlParam, "UTF-8"));
            HttpResponse response = httpclient.execute(httpost);
            String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
            logger.info("json是:" + jsonStr);

            if (jsonStr.indexOf("FAIL") != -1) {
                return null;
            }
            Map<String, String> map = XMLUtil.doXMLParse(jsonStr);

            prepay_id = (String) map.get("prepay_id");
        } catch (Exception e) {
            logger.error("统一支付提交异常:", e);
            return null;
        }
        return prepay_id;
    }




package com.wholesale.mall.utils.http;

import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DefaultHttpClient;

public class HttpClientConnectionManager {
	/**
	 * 获取SSL验证的HttpClient
	 * @param httpClient
	 * @return
	 */
	public static HttpClient getSSLInstance(HttpClient httpClient){
		ClientConnectionManager ccm = httpClient.getConnectionManager();
		SchemeRegistry sr = ccm.getSchemeRegistry();
		sr.register(new Scheme("https", MySSLSocketFactory.getInstance(), 443));
		httpClient =  new DefaultHttpClient(ccm, httpClient.getParams());
		return httpClient;
	}
	
	/**
	 * 模拟浏览器post提交
	 * 
	 * @param url
	 * @return
	 */
	public static HttpPost getPostMethod(String url) {
		HttpPost pmethod = new HttpPost(url); // 设置响应头信息
		pmethod.addHeader("Connection", "keep-alive");
		pmethod.addHeader("Accept", "*/*");
		pmethod.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
		pmethod.addHeader("Host", "api.mch.weixin.qq.com");
		pmethod.addHeader("X-Requested-With", "XMLHttpRequest");
		pmethod.addHeader("Cache-Control", "max-age=0");
		pmethod.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
		return pmethod;
	}

	/**
	 * 模拟浏览器GET提交
	 * @param url
	 * @return
	 */
	public static HttpGet getGetMethod(String url) {
		HttpGet pmethod = new HttpGet(url);
		// 设置响应头信息
		pmethod.addHeader("Connection", "keep-alive");
		pmethod.addHeader("Cache-Control", "max-age=0");
		pmethod.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
		pmethod.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/;q=0.8");
		return pmethod;
	}
}



package com.wholesale.mall.utils.http;

import com.wholesale.mall.weichart.MyX509TrustManager;
import org.apache.http.conn.ssl.SSLSocketFactory;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;

public class MySSLSocketFactory extends SSLSocketFactory {

	static {
		mySSLSocketFactory = new MySSLSocketFactory(createSContext());
	}

	private static MySSLSocketFactory mySSLSocketFactory = null;

	private static SSLContext createSContext() {
		SSLContext sslcontext = null;
		try {
			sslcontext = SSLContext.getInstance("SSL");
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		try {
			sslcontext.init(null,
					new TrustManager[] { new MyX509TrustManager() }, null);
		} catch (KeyManagementException e) {
			e.printStackTrace();
			return null;
		}
		return sslcontext;
	}

	@SuppressWarnings("deprecation")
	private MySSLSocketFactory(SSLContext sslContext) {
		super(sslContext);
		this.setHostnameVerifier(ALLOW_ALL_HOSTNAME_VERIFIER);
	}

	public static MySSLSocketFactory getInstance() {
		if (mySSLSocketFactory != null) {
			return mySSLSocketFactory;
		} else {
			return mySSLSocketFactory = new MySSLSocketFactory(createSContext());
		}
	}

}

如何解析xml:
需要引入jar包:

	<dependency>
        <groupId>org.jdom</groupId>
        <artifactId>jdom</artifactId>
        <version>1.1</version>
    </dependency>

解析xml代码:

package com.wholesale.mall.weichart;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;


public class XMLUtil {
	/**
	 * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
	 * @param strxml
	 * @return
	 * @throws JDOMException
	 * @throws IOException
	 */
	public static Map<String,String> doXMLParse(String strxml) throws JDOMException, IOException {
		strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");

		if(null == strxml || "".equals(strxml)) {
			return null;
		}
		
		Map<String,String> m = new HashMap<String,String>();
		
		InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
		SAXBuilder builder = new SAXBuilder();
		Document doc = builder.build(in);
		Element root = doc.getRootElement();
		List list = root.getChildren();
		Iterator it = list.iterator();
		while(it.hasNext()) {
			Element e = (Element) it.next();
			String k = e.getName();
			String v = "";
			List children = e.getChildren();
			if(children.isEmpty()) {
				v = e.getTextNormalize();
			} else {
				v = XMLUtil.getChildrenText(children);
			}
			m.put(k, v);
		}
		//关闭流
		in.close();
		return m;
	}
	
	/**
	 * 获取子结点的xml
	 * @param children
	 * @return String
	 */
	@SuppressWarnings("unchecked")
	public static String getChildrenText(List children) {
		StringBuffer sb = new StringBuffer();
		if(!children.isEmpty()) {
			Iterator it = children.iterator();
			while(it.hasNext()) {
				Element e = (Element) it.next();
				String name = e.getName();
				String value = e.getTextNormalize();
				List list = e.getChildren();
				sb.append("<" + name + ">");
				if(!list.isEmpty()) {
					sb.append(XMLUtil.getChildrenText(list));
				}
				sb.append(value);
				sb.append("</" + name + ">");
			}
		}
		return sb.toString();
	}
	
}


8.根据预支付标识,再次生成新的签名:

	 SortedMap<String, String> packageParam = new TreeMap<String, String>();
     String appid2 = weiChart.getAppId();
     String timestamp = Sha1Util.getTimeStamp();
     String nonceStr2 = nonce_str;
     String packages = "prepay_id=" + prepay_id;
     packageParam.put("appId", appid2);
     packageParam.put("timeStamp", timestamp);
     packageParam.put("nonceStr", nonceStr2);
     packageParam.put("package", packages);
     packageParam.put("signType", "MD5");
     String finalSign = Tools.createSign2(packageParam,wx_pertnerKey);
     logger.info("提交页面数据:appid=" + appid2 + "&timeStamp=" + timestamp + "&nonceStr=" + nonceStr2 + "&package=" + packages + "&sign=" + finalSign);

9.timeStamp生成方法:

public static String getTimeStamp() {
		return String.valueOf(System.currentTimeMillis() / 1000);
	}

10.最后把下列数据返回给前端,调起微信支付:

    mapVal.put("orderId", prepay_id);
   	mapVal.put("appId",appid2);
   	mapVal.put("timestamp",timestamp);
   	mapVal.put("nonceStr",nonceStr2);
   	mapVal.put("packages",packages);
   	mapVal.put("signType","MD5");
   	mapVal.put("sign",finalSign);
总结

微信支付对于新手来说,的确很难入手,难点在生成签名算法那,运用了MD5加密等一系列操作,如果有出现错误或异常,一定要记得加日志,查看日志信息,可以很明确的找出问题。我使用的是日志是: org.apache.log4j.Logger,还有很多其他方法,可以网上搜索下。

本篇文章使用的是传统写法,当然现在GitHub上有很多人封装了SDK,那个使用应该比较简单,都是封装好的,直接调用就行了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值