公众号微信支付开发手记

  作者:wallimn 时间:2017-02-27

  本人原创,欢迎转载,转载请保留本文链接。本文地址:http://wallimn.iteye.com/blog/2359379

  花了两天时间,琢磨了一下微信支付。感觉坑并不像传言中那么多,主要有两个:
  一、app的key与商户的key是两个不一样的key。我用了错误的key,花了很长时间解决支付时签名错误。
  二、最后页面调起支付时,一直报错:get_brand_wcpay_request:fail。花了很长时间排除。
  由于页面在手机上,很难调试。后来想到一个办法,在页面上建一个div元素,设置其ID为actionResult,然后使用jQuery提供的方法显示调用结果:
$("#actionResult").text(JSON.stringify(res)) 
  可以很容易地看到出错原因。
  常见的原因有三个:
  1、参数数量不足或名称错误。查看开发文档,提供足够的参数,名称、大小写要正确,注意timeStamp的单位为秒。 
  2、签名不正确。注意查看签名的要求。
  3、地址没有授权、或填写错误。“微信支付”->“开发配置”中,设置授权目录。授权要较长时间才能生效。如果地址为:http://wallimn.iteye.com/weixin/paytest,那么微信平台中的授权地址应为:http://wallimn.iteye.com/weixin/paytest

  还有两个要注意的小问题:
  一、定单号(out_trade_no)如果重复会导致支付失败。
  二、测试用户要加入白名单。

  微信的流程图有一点点儿不太直观,我画了一个简单的,应该一看就会明白。





  程序的主体代码如下。

一、几个基本的辅助类
1.统一定单类
package com.wallimn.weixin;
/**
 * 统一定单,用于向微信提供信息,进行支付
 * @author wallimn
 *
 */
public class UnifiedOrder {

	private String appid;
	private String mch_id;
	private String nonce_str;
	private String sign_type;//="MD5"
	private String body;//商品描述
	private String device_info="WEB";//设备号
	
	private String detail;
	private String out_trade_no;//商户订单号
	private String fee_type;//,非必需="CNY"
	private String total_fee;//分为单位
	
	private String spbill_create_ip;
	private String notify_url;
	private String trade_type;//JSAPI、NATIVE、APP三种
	private String product_id="something";//商品ID,非必需
	private String openid;//JSAPI时,必传
	public String getAppid() {
		return appid;
	}
	public void setAppid(String appid) {
		this.appid = appid;
	}
	public String getMch_id() {
		return mch_id;
	}
	public void setMch_id(String mch_id) {
		this.mch_id = mch_id;
	}
	public String getNonce_str() {
		return nonce_str;
	}
	public void setNonce_str(String nonce_str) {
		this.nonce_str = nonce_str;
	}
	public String getBody() {
		return body;
	}
	public void setBody(String body) {
		this.body = body;
	}
	public String getDetail() {
		return detail;
	}
	public void setDetail(String detail) {
		this.detail = detail;
	}
	public String getOut_trade_no() {
		return out_trade_no;
	}
	public void setOut_trade_no(String out_trade_no) {
		this.out_trade_no = out_trade_no;
	}
	public String getFee_type() {
		return fee_type;
	}
	public void setFee_type(String fee_type) {
		this.fee_type = fee_type;
	}
	public String getSpbill_create_ip() {
		return spbill_create_ip;
	}
	public void setSpbill_create_ip(String spbill_create_ip) {
		this.spbill_create_ip = spbill_create_ip;
	}
	public String getNotify_url() {
		return notify_url;
	}
	public void setNotify_url(String notify_url) {
		this.notify_url = notify_url;
	}
	public String getTrade_type() {
		return trade_type;
	}
	public void setTrade_type(String trade_type) {
		this.trade_type = trade_type;
	}
	public String getProduct_id() {
		return product_id;
	}
	public void setProduct_id(String product_id) {
		this.product_id = product_id;
	}
	public String getOpenid() {
		return openid;
	}
	public void setOpenid(String openid) {
		this.openid = openid;
	}
	public String getDevice_info() {
		return device_info;
	}
	public void setDevice_info(String device_info) {
		this.device_info = device_info;
	}
	public String getTotal_fee() {
		return total_fee;
	}
	public void setTotal_fee(String total_fee) {
		this.total_fee = total_fee;
	}
	public String getSign_type() {
		return sign_type;
	}
	public void setSign_type(String sign_type) {
		this.sign_type = sign_type;
	}
	
}

2.统一定单结果类
package com.wallimn.weixin;

/**
 * 微信定单返回信息
 * @author wallimn
 *
 */
public class UnifiedOrderResult {
	
	private String return_code;
	private String return_msg;
	private String appid;
	private String mch_id;
	private String nonce_str;
	private String sign;
	private String result_code;
	private String prepay_id;
	private String trade_type;
	

	public String getReturn_msg() {
		return return_msg;
	}

	public void setReturn_msg(String return_msg) {
		this.return_msg = return_msg;
	}

	public String getReturn_code() {
		return return_code;
	}

	public void setReturn_code(String return_code) {
		this.return_code = return_code;
	}

	public String getNonce_str() {
		return nonce_str;
	}

	public void setNonce_str(String nonce_str) {
		this.nonce_str = nonce_str;
	}

	public String getAppid() {
		return appid;
	}

	public void setAppid(String appid) {
		this.appid = appid;
	}

	public String getMch_id() {
		return mch_id;
	}

	public void setMch_id(String mch_id) {
		this.mch_id = mch_id;
	}

	public String getSign() {
		return sign;
	}

	public void setSign(String sign) {
		this.sign = sign;
	}

	public String getResult_code() {
		return result_code;
	}

	public void setResult_code(String result_code) {
		this.result_code = result_code;
	}

	public String getPrepay_id() {
		return prepay_id;
	}

	public void setPrepay_id(String prepay_id) {
		this.prepay_id = prepay_id;
	}

	public String getTrade_type() {
		return trade_type;
	}

	public void setTrade_type(String trade_type) {
		this.trade_type = trade_type;
	}
}

3.与微信相关的常量类
package com.wallimn.weixin;

import java.net.URLEncoder;

/**
 * 与微信相关的常量
 * @author Administrator
 *
 */
public class Config {
	public static final String app_id="";
	public static final String app_sercet="";
	/**
	 * 微信登录授权时使用的state参数
	 */
	public static final String state="wallimn";
	/**
	 * 微信登录授权时使用的scope参数
	 */
	public static final String scope="snsapi_userinfo";
	
	
	/**
	 * 获取code的Servlet地址,微信平台提供
	 */
	public static final String code_url = "http://www.day2up.com/bzslql/sso/weixin";
	/**
	 * 默认的登录后跳转地址
	 */
	public static final String default_back_url = "/index.jsp";
	
	/**
	 * 微信支付的商户号,微信商户注册时获得
	 */
	public static final String mch_id="";
	/**
	 * 商户的api的key,在微信的商户平台中设置。与app的key不一样.这个是个坑!
	 */
	public static final String mch_secret="";
	/**
	 * 微信统一下单接口
	 */
	public static final String unifiedorder_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
	
	/**
	 * 用户支付成功后,微信平台的通知地址
	 */
	public static final String pay_notify_url="";
	
	/**
	 * 返回微信的获取code的地址。将backurl编码到地址中。授权成功后,自动返回backurl对应的地址。
	 * <br>
	 * 时间:2017年2月27日,作者:http://wallimn.iteye.com
	 * @param backUrl
	 * @return
	 */
	public static String getCodeUrl(String backUrl){
		try {
			String url = code_url;
			//这样可以灵活返回到指定的地址,不必在Servlet中硬编码
			if(backUrl!=null){
				url += "?backurl="+backUrl;
			}
			String ru = URLEncoder.encode(url,"utf-8");
			return "https://open.weixin.qq.com/connect/oauth2/authorize?appid="+app_id+"&redirect_uri="+ru+"&response_type=code&scope="+scope+"&state="+state+"#wechat_redirect";
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	/**
	 * 获取access_token的链接
	 * <br>
	 * 时间:2017年2月27日,作者:http://wallimn.iteye.com
	 * @param code
	 * @return
	 */
	public static String getTokenUrl(String code){
    	return "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+app_id+"&secret="+app_sercet+"&code="+code+"&grant_type=authorization_code";
    }
	
	/**
	 * 刷新access_token的链接
	 * @param token
	 * @return
	 * 作者:wallimn<br/>
	 * 时间:2017年1月22日<br/>
	 * 联系:[email protected]<br/>
	 */
	public static String getFreshTokenUrl(String token){
    	return "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid="+app_id+"&grant_type=refresh_token&refresh_token="+token;
    }
    
	/**
	 * 使用access_token获取用户信息的链接
	 * @param token
	 * @param openid
	 * @return
	 * 作者:wallimn<br/>
	 * 时间:2017年1月22日<br/>
	 * 联系:[email protected]<br/>
	 */
	public static String getUserInfoUrl(String token,String openid){
    	return "https://api.weixin.qq.com/sns/userinfo?access_token="+token+"&openid="+openid+"&lang=zh_CN";
    }
}


二、支付工具类
package com.wallimn.weixin;

import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 微信支付处理辅助类
 * @author http://wallimn.iteye.com
 *
 */
public class PayUtils {
	private static final Logger log = LogManager.getLogger(PayUtils.class);

	/**
	 * 解析统一定单结果数据。
	 * 
	 * @param xml
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	public static UnifiedOrderResult parseUnifiedOrderResult(String xml) {
		UnifiedOrderResult result = new UnifiedOrderResult();
		//AXReader reader = new SAXReader();
		Document doc=null;
		try {
			//doc = reader.read(xml);
			doc = DocumentHelper.parseText(xml);
			result.setReturn_code(((Element) doc.selectSingleNode("/xml/return_code")).getText());
			result.setReturn_msg(((Element) doc.selectSingleNode("/xml/return_msg")).getText());
			if("SUCCESS".equals(result.getReturn_code())){
				result.setAppid(((Element) doc.selectSingleNode("/xml/appid")).getText());
				result.setMch_id(((Element) doc.selectSingleNode("/xml/mch_id")).getText());
				result.setNonce_str(((Element) doc.selectSingleNode("/xml/nonce_str")).getText());
				//result.setOpenid(((Element) doc.selectSingleNode("/xml/openid")).getText());
				result.setSign(((Element) doc.selectSingleNode("/xml/sign")).getText());
				result.setResult_code(((Element) doc.selectSingleNode("/xml/result_code")).getText());
				result.setPrepay_id(((Element) doc.selectSingleNode("/xml/prepay_id")).getText());
				result.setTrade_type(((Element) doc.selectSingleNode("/xml/trade_type")).getText());
				
			}
			doc = null;
			//reader = null;
		} catch (DocumentException e) {
			log.error("解析统一定单返回结果错误:{}",e.getMessage());
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * 使用必须用户提供的参数,生成统计定单对象
	 * 
	 * @param body
	 * @param detail
	 * @param openid
	 * @param out_trade_no
	 * @param spbill_create_ip
	 * @param total_fee
	 * @param trade_type
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	public static UnifiedOrder getUnifiedOrder(String body, String detail, String openid, String out_trade_no,
			String spbill_create_ip, String total_fee, String trade_type) {
		UnifiedOrder order = new UnifiedOrder();
		/*
		 * String wx_order = Config.order_url;// 获取统一下单接口地址 String mchappid =
		 * Config.appid;// 商户appid String mchid = Config.mch_id;// 商户ID String
		 * wx_callback = Config.paycallback_url;// 获取微信支付回调接口 String wx_key =
		 * Config.appsercet;// 微信商户后台设置的key String app_mchid = Config.appid;//
		 * APP调起微信支付的商户ID String app_mchappid = Config.mch_id;// APP调起微信的APPID
		 */
		order.setAppid(Config.app_id);
		order.setBody(body);
		order.setMch_id(Config.mch_id);
		order.setNonce_str(UUID.randomUUID().toString().replace("-", ""));
		order.setNotify_url(Config.pay_notify_url);
		order.setOpenid(openid);
		order.setOut_trade_no(out_trade_no);
		order.setSpbill_create_ip(spbill_create_ip);
		order.setTotal_fee(total_fee);
		order.setTrade_type(trade_type);
		order.setDetail(detail);

		return order;
	}

	/**
	 * 将bean转化为Map,方便下一步签名、转化xml使用
	 * 
	 * @param bean
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	public static Map<String, String> BeanToMap(Object bean) {
		Map<String, String> map = null;
		try {
			map = BeanUtils.describe(bean);
			map.remove("class");
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return map;
	}

	/**
	 * 将Map数据转化为统一定单要求的xml数据。
	 * 
	 * @param map
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	public static String MapToXML(Map<String, String> map) {
		if (map == null)
			return null;
		StringBuilder sb = new StringBuilder();
		sb.append("<xml>");
		String value = null;
		Set<String> keySet = map.keySet();
		String [] keyArray = keySet.toArray(new String[0]);
		Arrays.sort(keyArray);

		for (String key :keyArray) {
			value = map.get(key);
			if (value == null) {
				value = "";
			} else if (value.length() > 60) {
				// 简单处理一下,如果过长,就加上XML的转义
				value = "<![CDATA[" + value + "]]";
			}
			sb.append("<").append(key).append(">").append(value).append("</").append(key)
					.append(">");
		}
		sb.append("</xml>");
		return sb.toString();
	}
	
	/**
	 * 读取流中的xml数据,转化为Map<String,String>
	 * 
	 * @param is
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	public static Map<String,String> XmlToMap(InputStream is){
		HashMap<String, String> map = new HashMap<String, String>();
		log.info("------------微信回调函数----------------");
		// 1、读取传入信息并转换为map
		SAXReader reader = new SAXReader();
		Document document = null;
		try {
			document = reader.read(is);
			log.info("支付返回结果:/n/r"+document.asXML());
			Element root = document.getRootElement();
			List<Element> list = root.elements();
			for (Element e : list) {
				map.put(e.getName().trim(), e.getText().trim());
			}
			document = null;
			reader = null;
		}
		catch(Exception e){
			log.error("XML转化为bean错误,原因:{}",e.getMessage());
			e.printStackTrace();
		}
		return map;
		
	}

	/**
	 * 提交统一定单,仅对应于JSAPI类型
	 * 
	 * @param orderXml
	 * @return 返回结果为适合公众号页面上使用的JSON数据。格式与微信要求一致。
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	public static String postUnifiedOrder(String orderXml) {
		String response = "";
		try {// 注意,此处的httputil一定发送请求的时候一定要注意中文乱码问题,中文乱码问题会导致在客户端加密是正确的,可是微信端返回的是加密错误
			log.info("统一定单发送:"+Config.unifiedorder_url);
			response = HttpUtils.post(Config.unifiedorder_url, orderXml);
			log.info("统一定单结果:"+response);
			UnifiedOrderResult orderResult = PayUtils.parseUnifiedOrderResult(response);

			// 6、处理请求结果
			log.info("return_code={}, result_code={}",orderResult.getReturn_code(),orderResult.getResult_code());
			if ("SUCCESS".equals(orderResult.getReturn_code()) && "SUCCESS".equals(orderResult.getResult_code())) {
				log.error("微信支付统一下单请求成功:" + orderResult.getPrepay_id());
			} else {
				log.error("微信支付统一下单请求错误:{}" + orderResult.getReturn_msg());
				return null;
			}

			HashMap<String, String> back = new HashMap<String, String>();
			ObjectMapper mapper = new ObjectMapper();

			// 生成客户端调时需要的信息对象
			// 网页调起的时候
			//时戳,要求转化为以秒为单位
			String time = Long.toString(System.currentTimeMillis() / 1000);
			back.put("appId", Config.app_id);
			back.put("timeStamp", time);
			String uuid = UUID.randomUUID().toString().replace("-","");
			back.put("nonceStr", uuid);
			back.put("package", "prepay_id=" + orderResult.getPrepay_id());
			back.put("signType", "MD5");
			
			//根据以上参数,计算签名。
			String sign = signature(back, Config.mch_secret);
			back.put("paySign", sign);

			String json = mapper.writeValueAsString(back);
			//log.info("返回也面的JSON:/n/r{}"+json);
			return json;

		} catch (Exception e) {
			log.error("微信支付统一下单失败,http请求失败:{}", e.getMessage());
			e.printStackTrace();
		}

		return null;

	}

	/**
	 * 查检定单数据
	 * 
	 * @param order
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	public static boolean checkUnifiedOrder(UnifiedOrder order) {

//		if (StringUtils.isBlank(order.getDetail()) ) {
//			log.error("微信支付统一下单请求错误:detail为空!");
//			return false;
//		}

		if ( StringUtils.isBlank(order.getBody())) {
			log.error("微信支付统一下单请求错误:body为空!");
			return false;
		}

		if ( StringUtils.isBlank(order.getSpbill_create_ip())) {
			log.error("微信支付统一下单请求错误:spbill_create_ip为空!");
			return false;
		}

		if (StringUtils.isBlank(order.getOut_trade_no()) ) {
			log.error("微信支付统一下单请求错误:out_trade_no为空!");
			return false;
		}

		if (StringUtils.isBlank(order.getTotal_fee())) {
			log.error("微信支付统一下单请求错误:total_fee为空!");
			return false;
		}

		if (StringUtils.isBlank(order.getTrade_type())) {
			log.error("微信支付统一下单请求错误:trade_type为空!");
			return false;
		}

		try {// 进行格式转换异常获取,保证数目正确
			int fee = Integer.parseInt(order.getTotal_fee());
			if (fee == 0 || !order.getTotal_fee().equals(String.valueOf(fee))) {// 微信支付的支付金额必须为大于0的int类型,单位为分
				log.error("微信支付统一下单请求错误:请求金额不能为0,不能为小数");
				return false;
			}
		} catch (Exception e) {
			log.error("微信支付统一下单请求错误:请求金额格式错误,值:{}", order.getTotal_fee());
			return false;
		}

		if (!("JSAPI".equalsIgnoreCase(order.getTrade_type()) || "NATIVE".equalsIgnoreCase(order.getTrade_type())
				|| "APP".equalsIgnoreCase(order.getTrade_type()))) {
			log.error("微信支付统一下单请求错误:支付类型不正确,{}", order.getTrade_type());
			return false;
		}

		/* 公众号调起微信支付的时候,必须要有openID */
		if ("JSAPI".equalsIgnoreCase(order.getTrade_type()) && StringUtils.isBlank(order.getOpenid())) {
			log.error("微信支付统一下单请求错误:请求参数不足,当类型为JSAPI时,必须提供openid");
			return false;
		}

		if (StringUtils.isBlank(order.getAppid()) || StringUtils.isBlank(order.getMch_id())
				|| StringUtils.isBlank(order.getNotify_url())) {
			log.error("微信支付统一下单请求错误:系统配置信息缺失,appid, mch_id, notify_url");
			return false;
		}

		return true;
	}

	/**
	 * 签名生成的通用步骤如下:
	 * 第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
	 * 特别注意以下重要规则: ◆ 参数名ASCII码从小到大排序(字典序); ◆ 如果参数的值为空不参与签名; ◆ 参数名区分大小写; ◆
	 * 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。 ◆
	 * 微信接口可能增加字段,验证签名时必须支持增加的扩展字段
	 * 第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。
	 * key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 *
	 */
	
	/**
	 * 为微信支付发送数据进行签名的类。
	 * 
	 * @param map
	 * @param app_secret
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	public static String signature(Map<String, String> map, String app_secret) {
		List<String> keyList = new LinkedList<String>();
		// 如果参数不为空,加入签名的字符串
		for (Entry<String, String> entry : map.entrySet()) {
			if (StringUtils.isNotEmpty(entry.getValue())) {
				keyList.add(entry.getKey());
			}
		}

		String[] keyArray = keyList.toArray(new String[0]);
		Arrays.sort(keyArray);
		StringBuilder sbInfo = new StringBuilder();
		// 进行字典排序
		for (String str : keyArray) {
			sbInfo.append(str).append("=").append(map.get(str)).append("&");
		}
		if (StringUtils.isNotEmpty(app_secret)) {
			sbInfo.append("key=" + app_secret);
		}
		String tosend = sbInfo.toString();
		MessageDigest md = null;
		byte[] bytes = null;
		try {

			md = MessageDigest.getInstance("MD5");
			bytes = md.digest(tosend.getBytes("utf-8"));
		} catch (Exception e) {
			e.printStackTrace();
		}

		String sign = byteToStr(bytes);
		System.out.println("签名字符串:"+tosend);
		sign = sign.toUpperCase();
		System.out.println("签名字结果:"+sign);
		return sign;

	}

	/**
	 * 字节数组转换为字符串
	 * 
	 * @param byteArray
	 * @return
	 */
	private static String byteToStr(byte[] byteArray) {
		String strDigest = "";
		for (int i = 0; i < byteArray.length; i++) {
			strDigest += byteToHexStr(byteArray[i]);
		}
		return strDigest;
	}

	/**
	 * 字节转换为字符串
	 * 
	 * @param mByte
	 * @return
	 */
	private static String byteToHexStr(byte mByte) {
		char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
		char[] tempArr = new char[2];
		tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
		tempArr[1] = Digit[mByte & 0X0F];

		String s = new String(tempArr);
		return s;
	}
	/**
	 * SHA-1签名,这个在实际中并未使用
	 * <br>
	 * 时间:2017年2月27日,作者:http://wallimn.iteye.com
	 * @param map
	 * @return
	 */
	public static String signatureSHA1(Map<String, String> map) {
		Set<String> keySet = map.keySet();
		String[] str = new String[map.size()];
		StringBuilder tmp = new StringBuilder();
		// 进行字典排序
		str = keySet.toArray(str);
		Arrays.sort(str);
		for (int i = 0; i < str.length; i++) {
			String t = str[i] + "=" + map.get(str[i]) + "&";
			tmp.append(t);
		}

		String tosend = tmp.toString().substring(0, tmp.length() - 1);
		MessageDigest md = null;
		byte[] bytes = null;
		try {

			md = MessageDigest.getInstance("SHA-1");
			bytes = md.digest(tosend.getBytes("utf-8"));
		} catch (Exception e) {
			e.printStackTrace();
		}

		String singe = byteToStr(bytes);
		return singe.toLowerCase();

	}
}


三、HttpClient辅助类
package com.wallimn.weixin;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;

public class HttpUtils {
	private static final String Charset = "utf-8";
	public static HttpClient client;
	static {
		client = HttpClientBuilder.create().build();

	}

	/**
	 * 发送请求,如果失败,会返回null
	 * @param url
	 * @param map
	 * @return
	 */
	public static String post(String url, Map<String, String> map) {
		// 处理请求地址
		try {
			URI uri = new URI(url);
			HttpPost post = new HttpPost(uri);

			// 添加参数
			List<NameValuePair> params = new ArrayList<NameValuePair>();
			for (String str : map.keySet()) {
				params.add(new BasicNameValuePair(str, map.get(str)));
			}
			post.setEntity(new UrlEncodedFormEntity(params, Charset));
			// 执行请求
			HttpResponse response = client.execute(post);

			if (response.getStatusLine().getStatusCode() == 200) {
				// 处理请求结果
				StringBuffer buffer = new StringBuffer();
				InputStream in = null;
				try {
					in = response.getEntity().getContent();
					BufferedReader reader = new BufferedReader(new InputStreamReader(in,Charset));
					String line = null;
					while ((line = reader.readLine()) != null) {
						buffer.append(line);
					}

				} catch (UnsupportedOperationException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} finally {
					// 关闭流
					if (in != null)
						try {
							in.close();
						} catch (IOException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
				}

				return buffer.toString();
			} else {
				return null;
			}
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnsupportedEncodingException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (ClientProtocolException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		return null;

	}

	/**
	 * 发送请求,如果失败会返回null
	 * @param url
	 * @param str
	 * @return
	 */
	public static String post(String url, String str) {
		// 处理请求地址
		try {
			URI uri = new URI(url);
			HttpPost post = new HttpPost(uri);
			post.setEntity(new StringEntity(str, Charset));
			// 执行请求
			HttpResponse response = client.execute(post);

			if (response.getStatusLine().getStatusCode() == 200) {
				// 处理请求结果
				StringBuffer buffer = new StringBuffer();
				InputStream in = null;
				try {
					in = response.getEntity().getContent();
					BufferedReader reader = new BufferedReader(new InputStreamReader(in,"utf-8"));
					String line = null;
					while ((line = reader.readLine()) != null) {
						buffer.append(line);
					}

				} finally {
					// 关闭流
					if (in != null)
						in.close();
				}

				return buffer.toString();
			} else {
				return null;
			}
		} catch (URISyntaxException | IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;

	}

	/**
	 * 发送GET方式的请求,并返回结果字符串。
	 * <br>
	 * 时间:2017年2月27日,作者:http://wallimn.iteye.com
	 * @param url
	 * @return 如果失败,返回为null
	 */
	public static String get(String url) {
		try {
			URI uri = new URI(url);
			HttpGet get = new HttpGet(uri);
			HttpResponse response = client.execute(get);
			if (response.getStatusLine().getStatusCode() == 200) {
				StringBuffer buffer = new StringBuffer();
				InputStream in = null;
				try {
					in = response.getEntity().getContent();
					BufferedReader reader = new BufferedReader(new InputStreamReader(in,Charset));
					String line = null;
					while ((line = reader.readLine()) != null) {
						buffer.append(line);
					}

				} finally {
					if (in != null)
						in.close();
				}

				return buffer.toString();
			} else {
				return null;
			}
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClientProtocolException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;

	}

}


四、Controller中相关的代码
package com.wallimn.bzslql.controller;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.h2.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.wallimn.weixin.Config;
import com.wallimn.weixin.HttpUtils;
import com.wallimn.weixin.PayUtils;
import com.wallimn.weixin.UnifiedOrder;
import com.wallimn.weixin.User;

/**
 * 与微信相关的处理Controller
 * 
 * @author wallimn
 *
 */
@Controller
@RequestMapping("/weixin")
public class WeixinController extends CommonController {

	private static Logger log = LogManager.getLogger(WeixinController.class);
	@Autowired
	private HttpServletRequest request;
	@Autowired
	protected HttpServletResponse response;

	/**
	 * 接受用户提供的信息,生成标准的统一订单,提交到微信平台进行处理,提交成功后,返回JSON数据,
	 * 公众号网页依据此JSON内容,调用微信平台的接口、输入密码,完成支付
	 * 
	 * @param body
	 * @param detail
	 * @param out_trade_no
	 * @param total_fee
	 * @return
	 * @throws Exception
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	@RequestMapping("/unifiedorder")
	@ResponseBody
	public String unifiedorder(String body, String detail, String out_trade_no, String total_fee) throws Exception {
		String spbill_create_ip = request.getRemoteAddr();
		String openid = this.getLoginUserId();
		log.info("创建定单:body:{}, detail:{},out_trade_no:{},total_fee:{}", body, detail, out_trade_no, total_fee);
		UnifiedOrder order = PayUtils.getUnifiedOrder(body, "", openid, out_trade_no, spbill_create_ip, total_fee,
				"JSAPI");
		if (PayUtils.checkUnifiedOrder(order) == true) {
			Map<String, String> map = PayUtils.BeanToMap(order);
			String sign = PayUtils.signature(map, Config.mch_secret);
			log.info("签名:" + sign);
			map.put("sign", sign);//加入签名
			String orderXml = PayUtils.MapToXML(map);//生成xml定单数据
			log.info("统一定单:" + orderXml);
			String result = PayUtils.postUnifiedOrder(orderXml);
			log.info("统一定单结果:" + result);
			// String result = WechatOrderUtils.createOrder(detail, body,
			// openid, ip, goodSn, orderSn, total_fee, "JSAPI");
			return result;
		} else {
			return this.getFailJSON("FAIL", "定单参数错误!");
		}
	}

	/**
	 * 微信支付回调函数。 当用户支付成功后,通过此地址通知操作结果
	 * 该地址在向微信发送统一定单时,告知微信平台。
	 * 
	 * @param request
	 * @param response
	 * @throws IOException
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	@RequestMapping(value = "/paycallback", method = RequestMethod.POST)
	public void payCallBack(HttpServletRequest request, HttpServletResponse response) throws IOException {
		InputStream is = request.getInputStream();
		Map<String, String> map = PayUtils.XmlToMap(is);
		String result_code = map.get("result_code");
		// 支付成功
		if ("SUCCESS".equals(result_code)) {
			String sign = map.get("sign");
			// System.out.println(map.toString());
			// 2、克隆传入的信息并进行验签
			map.remove("sign");
			String sign2 = PayUtils.signature(map, Config.mch_secret);
			if (StringUtils.equals(sign, sign2)) {
				log.info("微信支付回调函数,验答正确。处理业务逻辑。");
				// TODO:添加业务处理逻辑
			} else {
				log.error("微信支付回调函数:验签错误!");

			}
		} else if ("FAIL".equals(result_code)) {
			log.error("微信支付回调函数:失败!失败代码为:{}", map.get("return_code"));

		}

		// 返回信息,防止微信重复发送报文
		// 由于微信后台会同时回调多次,所以需要做防止重复提交操作的判断
		// 此处放防止重复提交操作
		String result = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
				+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>";
		PrintWriter out = new PrintWriter(response.getOutputStream());
		out.print(result);
		out.flush();
		out.close();

	}

	/**
	 * 支付操作准备界面(测试用)。
	 * 注意映射地址为:/weinxin/paytest/170228,那么在微信设置界面中,填写的授权地址应为:/weinxin/paytest/
	 * 这是一个坑
	 * 
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	@RequestMapping("/paytest/170228")
	public String paytest() {
		return "paytest";
	}

	/**
	 * 支付操作准备界面。
	 * 注意映射地址为:/weinxin/paytest/170228,那么在微信设置界面中,填写的授权地址应为:/weinxin/payreal/
	 * 
	 * @return
	 * <br>
	 * 时间:2017年2月27日<br>
	 * 作者:http://wallimn.iteye.com<br>
	 * 联系:[email protected]
	 */
	@RequestMapping("/payreal/170228")
	public String payreal() {
		return "payreal";
	}
}


五、公众号页面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<!--
	作者: wallimn, http://wallimn.iteye.com
	时间:2017年1月26日
	功能:
-->
<html lang="zh-cn">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>JSAPI方式支付调起页面</title>
	<c:set var="ctx" value="${pageContext.request.contextPath}"></c:set>
    <script type="text/javascript" src="${ctx }/js/jquery-1.11.1.min.js"></script>
    <script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
    <script type="text/javascript">
        function submit(){
        	$("#actionResult").text("");
            $.ajax({
                type: 'POST',
                url: '${ctx }/weixin/unifiedorder?dt='+(new Date()).getTime(),
                data: {'detail':'测试detail','body':'商户名-商品名','out_trade_no':(new Date()).getTime().toString(16),'total_fee':$("input[name=total_fee]").val()},
                success: function(json){
	                if(!json || !json.appId){
	                	$("#actionResult").text("参数错误!");
	                	return ;
	                }
	               wxpay(json);
            	},
                dataType: "json"});

        }
		function wxpay(json){
			function onBridgeReady(){
				   WeixinJSBridge.invoke(
				       'getBrandWCPayRequest', {
				           "appId" :json.appId,     //公众号名称,由商户传入     
				           "timeStamp":json.timeStamp,         //时间戳,自1970年以来的秒数     
				           "nonceStr" :json.nonceStr, //随机串     
				           "package" : json.package,     
				           "signType" :json.signType,         //微信签名方式:     
				           "paySign" :json.paySign //微信签名 
				       },
				       function(res){     
				           if(res.err_msg == "get_brand_wcpay_request:ok" ) {
				        	   $("#actionResult").text("支付成功!");
				           }     // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。 
				           else{
				        	   $("#actionResult").text("支付失败:"+JSON.stringify(res));
				           }
				       }
				   ); 
				}
				if (typeof WeixinJSBridge == "undefined"){
				   if( document.addEventListener ){
				       document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
				   }else if (document.attachEvent){
				       document.attachEvent('WeixinJSBridgeReady', onBridgeReady); 
				       document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
				   }
				}else{
				   onBridgeReady();
				}		
		}
        $(function(){
                $("#sub").click(function(){
                    submit();
                });
                $("#actionResult").text("WeixinJSBridge");
        });

    </script>
</head>
<body>
<div style="text-align:center">
	<div id="actionResult">注意单位为(分)</div>
	<input type="text" placeholder="请输入金额(分)" name="total_fee" value="1"><br>
	<input type="button" id="sub" value="支付">
</div>
</body>
</html>

猜你喜欢

转载自wallimn.iteye.com/blog/2359379