微信公众号支付/退款(java环境)开发介绍

开发之前翻阅了很多帖子,结合自己的实际开发情况,将微信支付/退款 流程以及code贴出,希望通过这一篇帖子就能解决你的问题,有不清楚的直接留言,我会及时回复(ง •̀_•́)ง得意

一些说明:xxxUtils为工具类,Constant为常量类

为方便开发,所用和微信支付相关code(包括工具类)文中均贴出。项目采用的是SSM框架,maven进行管理的

一、开发前准备

1.微信官方要求域名必须通过icp备案,且连接方式从2018.01.01起不再支持HTTP连接,仅支持HTTPS

所以需要:1.在icp备案官网或第三方网站申请域名备案2.申请SSL证书从而获得HTTPS连接,推荐在腾讯云上申请免费版,有效期1年

https://cloud.tencent.com/product/ssl?from=qcloudHpHeaderSsl

对于不同服务器对应不同的证书格式以及方法可以参考下面连接 

https://www.wosign.com/support/ssl-install-index.htm

以tomcat服务器为例:

扫描二维码关注公众号,回复: 3632275 查看本文章

修改server.xml将默认的localhost访问修改为https+域名访问

以下几点一开始可能一头雾水,不知道该如何配置,可以先放一放,等开发到相应步骤自然需要填写

需要配置的2个平台地址

商户平台:https://pay.weixin.qq.com/

公众平台:https://mp.weixin.qq.com/

2.配置微信支付目录

参考官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3

假设调用微信H5支付页面的地址为https://a.b.com/pay

那么:授权目录配置为https://a.b.com/

3.配置微信公众号域名(业务域名、JS接口安全域名、网页授权域名)

参考官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3

4.API安全密钥

支付签名所需要拼接的参数,即后文sign拼接所需要的key

参考官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3

5.下载商户证书

参考官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3

退款需要调用商户证书进行验证

Java环境采用apiclient_cert.p12 证书密码为商户号

二、开发流程

其中红色部分是需要我们开发的地方,其余部分均为微信功能

具体流程:用户点击支付按钮-->后台逻辑处理-->前台接收数据并调用微信JS唤起支付控件-->出现输入密码界面,包含金额等一些信息-->输入密码后出现微信的支付成功页面(微信自己处理)-->回调我们设置的商户界面(同时后台也会通知我们支付结果)

我们所需要做的事情:

1.获取用户授权,拿到openId

2.调用微信统一下单接口获取预支付id

3.将数据发送给前台,调用微信内置JS唤起支付控件

4.支付完成后,微信回调URL的处理

5.微信后台异步通知商户支付结果,商户收到消息后需要告知微信处理结果

6.根据功能需求的其他业务逻辑,比如DB的交互之类

三、具体开发步骤

1.获取用户授权,拿到openId

参考官方文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842

这篇官方文档介绍的还是比较详细的,可以仔细研究下

大概流程如下:

第一步:用户同意授权,获取code

访问如下链接:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

每个参数的具体含义参照上方官方文档,其中几个注意点:微信支付情景下scope设置为snsapi_base即可,可以获得openId;redirect_uri需要urlEncode处理。跳转回调redirect_uri,应当使用https链接来确保授权code的安全性。  

参数顺序必须正确。

如果用户同意授权,页面将跳转至redirect_uri/?code=CODE&state=STATE

redirect_uri一般为controller,拿到code后在其中做后续步骤,如wxpay.xxx.com/wechat/unifiedOrder

code说明: code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。

第二步:通过code换取网页授权access_token

在 wxpay.xxx.com/wechat/unifiedOrder  Controller中做后续逻辑

String code = request.getParameter("code");

获取code后,请求以下链接获取access_token:

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

返回数据为JSON,具体含义参照官方文档

我这里采用了jackson工具将json转化为实体类进行操作,代码如下:

获取返回数据的工具类:

public static AuthToken getTokenByAuthCode(String code) {
		AuthToken authToken = null;
		StringBuilder json = new StringBuilder();
		try {
			URL url = new URL(Constant.Authtoken_URL(code));
			URLConnection urlConnection = url.openConnection();
			urlConnection.connect();
			BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
			String inputLine;
			while ((inputLine = in.readLine()) != null) {
				json.append(inputLine);
			}
			in.close();
			// 将json文本转化为authToken对象
			authToken = jsonToEntity(json.toString(), AuthToken.class);
		} catch (IOException e) {
			logger.error("*****获取access_token异常*****");
			e.printStackTrace();
		}
		return authToken;
	}

Json转化实体类工具类:

public static <T> T jsonToEntity(String jsonString, Class<T> entityType) {
		T entity = null;
		try {
			entity = jsonObjectMapper.readValue(jsonString, entityType);
		} catch (Exception e) {
			logger.error("*****json转化异常*****");
			e.printStackTrace();
		}
		return entity;
	}

AuthToken是返回数据的实体类

2.调用微信统一下单接口获取预支付id

简单的理解就是调用一个微信的API接口,它需要很多的参数,赋值拼接后转化为XML格式发送给微信,微信再返回我们XML格式的响应报文。

由于参数很多并且复杂,开发前一定要详读官方API文档。

官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

下面对用到的字段做具体的讲解(注意大小写!):

appid==公众账号ID==微信公众号后台查看

mch_id==商户号==微信支付平台查看

device_info==设备号==公众号支付传“WEB”

nonce_str==32位随机字符串==微信支付API接口协议中包含字段nonce_str,主要保证签名不可预测。生成code如下:

public static String generateUUID() {
		return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
	}

sign==签名==先跳过,等其他参数赋值结束后再讲sign

sign_type==签名类型==采用“MD5”

body==商品描述==传中文可能出现问题,注意UTF-8编码

attach==附加数据==在查询API和支付通知中原样返回,可作为自定义参数使用

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

我采用的是当前14位系统时间+4位随机数构成订单号,代码如下:

/**
	 * 生成订单号 yyyyMMddHHmmss+4位随机数 共18位
	 * 适用于订单号和退款单号
	 */
	public static String generateOut_trade_no() {
		Date date = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
		String format = sdf.format(date);
		Random random = new Random();
		String result = "";
		for (int i = 0; i < 4; i++) {
			result += random.nextInt(10);
		}
		String finalResult = format + result;
		logger.info("订单号:" + finalResult);
		return finalResult;
	}

fee_type==标价币种==默认“CNY”,可以不传

total_fee==标价金额==单位为分!!注意做元分转化

spbill_create_ip==终端ip==springmvc中可以采用request.getRemoteAddr()获得

time_start==交易起始时间==订单生成时间,格式为yyyyMMddHHmmss

time_expire==交易结束时间==订单失效时间,格式为yyyyMMddHHmmss,最短失效时间要超过1分钟

notify_url==通知地址==异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。(现在可能不知道做什么用的,也不知道怎么配,没关系,等做到后面微信通知结果就豁然开朗了,可以先随便赋值)

trade_type==交易类型==公众号支付传“JSAPI

openid==用户标识==上一步获得的openid

微信签名算法详解:(提示签名错误很正常,仔细检查拼接顺序,大小写)

参考文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3

第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA

第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue

得到sign后赋值,并将所有参数封装成XML

上图就是将微信所需要的所有参数赋值并放入实体类(除sign外),然后将该实体类转化为map并通过工具类生成签名,具体code如下:

//实体类转化为SortedMap
	private SortedMap<String, Object> buildParamMap(PaySendData data) {
		SortedMap<String, Object> paramters = new TreeMap<String, Object>();
		Field[] fields = data.getClass().getDeclaredFields();
		try {
			for (Field field : fields) {
				field.setAccessible(true);
				if (null != field.get(data)) {
					paramters.put(field.getName().toLowerCase(), field.get(data).toString());
				}
			}
		} catch (Exception e) {
			logger.error("构建签名map错误: ");
			e.printStackTrace();
		}
		return paramters;
	}

获得签名

public static String getSign(SortedMap<String, Object> map) {
		StringBuffer buffer = new StringBuffer();
		Set<Map.Entry<String, Object>> set = map.entrySet();
		Iterator<Map.Entry<String, Object>> iterator = set.iterator();
		while (iterator.hasNext()) {
			Map.Entry<String, Object> entry = iterator.next();
			String k = entry.getKey();
			Object v = entry.getValue();
			// 参数中sign、key不参与签名加密
			if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
				buffer.append(k + "=" + v + "&");
			}
		}
		buffer.append("key=" + Constant.KEY);
		//System.out.println(buffer.toString());
		String sign = MD5(buffer.toString()).toUpperCase();
		logger.info("sign:" + sign);
		return sign;
	}

现在所有参数+签名都已经获得并注入实体类,封装成XML,采用XStream

由于XStream本身不支持带有“_”的节点,而微信参数中带有“_”,所以首先要让其支持

public static XStream xStream = new XStream(new DomDriver("UTF-8", new XmlFriendlyNameCoder("-_", "_")));

之后使用xStream对象做序列化操作

public static String sendDataToXml(PaySendData data) {
		xStream.autodetectAnnotations(true);
		xStream.alias("xml", PaySendData.class);
		String xmlData = xStream.toXML(data);
		logger.info(xmlData);
		return xmlData;
	}

PS:在PaySendData实体类中需要使用@XStreamAlias("xxx")注解,xxx即想要序列化后的名字

到此,得到了微信所需要的XML封装好的参数,下面调用微信的统一下单地址:https://api.mch.weixin.qq.com/pay/unifiedorder

这里采用apache的httpclient进行连接

try {
			// 发送POST统一下单请求
			CloseableHttpResponse response = HttpUtil.Post(Constant.UNIFIED_ORDER_URL, reqXml, false);
			try {
				resultMap = PayUtils.parseXml(response.getEntity().getContent());
				//TODO 最终删除
				logger.info(resultMap.toString());
				// 关闭流
				EntityUtils.consume(response.getEntity());
			} finally {
				response.close();
			}
		} catch (Exception e) {
			logger.error("*****微信支付统一下单异常*****");
			e.printStackTrace();
		}

xml格式的流转化为map集合

public static Map<String, Object> parseXml(InputStream inputStream) {
		SortedMap<String, Object> map = new TreeMap<String, Object>();
		try {
			// 获取request输入流
			SAXReader reader = new SAXReader();
			Document document = reader.read(inputStream);
			// 得到xml根元素
			Element root = document.getRootElement();
			// 得到根元素所有节点
			List<Element> elementList = root.elements();
			// 遍历所有子节点
			for (Element element : elementList) {
				map.put(element.getName(), element.getText());
			}
			// 释放资源
			inputStream.close();
		} catch (Exception e) {
			e.printStackTrace();
			logger.error("*****微信工具类:解析xml异常*****");
		}
		return map;
	}

httpclientpost请求

/**
	 * 发送post请求
	 * 
	 * @param url
	 *            请求地址
	 * @param outputEntity
	 *            发送内容 xml字符串
	 * @param isLoadCert
	 * 			是否加载证书
	 * @throws IOException 
	 * @throws ClientProtocolException 
	 */
	public static CloseableHttpResponse Post(String url,String outputEntity,boolean isLoadCert) throws Exception {
		HttpPost httpPost=new HttpPost(url);
		// 得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别
		httpPost.addHeader("Content-Type", "text/xml");
		httpPost.setEntity(new StringEntity(outputEntity,"UTF-8"));
		if(isLoadCert) {
			//加载含有证书的http请求
			return HttpClients.custom().setSSLSocketFactory(CommonsUtils.initCert()).build().execute(httpPost);
		}else {
			return HttpClients.custom().build().execute(httpPost);
		}
	}

需要注意的是这个工具类在之后退款也需要使用,当前付款无需加载证书,而退款需要加载证书,加载证书的工具类CommonsUtils.initCert()在后面会放上。这些方法均来自微信官方sdk模板,放心使用。

到此为止,我们得到了返回结果的map集合,终于拿到了所需要的prepayid(当然,首先需要判断返回数据中的“return_code”以及“result_code”

3.将数据发送给前台,调用微信内置JS唤起支付控件

参考官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6

下面对参数做具体的讲解:

appId==公众号id==同之前下单所使用的(特别注意这里的I是大写前面下单的是小写)

timeStamp==时间戳==标准北京时间,时区为东八区,自1970年1月1日0点0分0秒以来的秒数。注意:需要转换成秒(10位数字)

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

nonceStr==随机字符串==同之前下单的nonceStr,可以一样,也可以重新生成一个

public static String generateUUID() {
		return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
	}

package==统一下单接口返回的prepay_id参数值,提交格式如:prepay_id=***

上一步千辛万苦获得的prepayid就是用在这里的~

signType==签名方式==同之前下单所用的签名方式“MD5”

paySign==签名==签名方式同下单,具体代码可以参考上面的getSign(SortedMap<String,Object> map)方法

现在将这6个参数传递给前台H5支付页面并调起支付,前台JS代码如下:

function onBridgeReady() {
			WeixinJSBridge.invoke(
							'getBrandWCPayRequest',
							{
								"appId" : appId,
								"timeStamp" : timeStamp,
								"nonceStr" : nonceStr,
								"package" : prepayId,
								"signType" : "MD5",
								"paySign" : paySign
							},
							function(res) {
								if (res.err_msg == "get_brand_wcpay_request:ok") {
									location.href = "xxxx";
								} else {//这里支付失败和支付取消统一处理
									location.href = "xxxxxx";
								}
							});
		}

		$(document).ready(
				function() {
					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();
					}
				});

到此为止如果操作正常应该是会出现下面的界面

在正确输入密码后会出现下面界面

到此,该步骤结束

4.支付完成后,微信回调URL的处理

在上步的JS代码中

function(res) {
	if (res.err_msg == "get_brand_wcpay_request:ok") {
		location.href = "xxxx";
	}else{//这里支付失败和支付取消统一处理
		location.href = "xxxx";
		}
	}

对于get_brand_wcpay_request:ok以及else的逻辑处理,如跳转回商户自己定义的一个成功页面。

5.微信后台异步通知商户支付结果,商户收到消息后需要告知微信处理结果

官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7

需要注意的是,微信前台的回调和后台的通知先后顺序是不保证的,JSAPI返回值作为触发商户网页跳转的标志,但商户后台应该只在收到微信后台的支付成功回调通知后,才做真正的支付成功的处理。

第一步:验证签名

微信会回调我们在下单时配置的notify_url,如果之前是随便配置的,现在就要改回来啦。微信是以流的形式将数据返回,我们接收流解析后,需要将其中的参数重新签名并验证,保证这个信息是微信官方返回给我们的,防止假通知。

第二步:商户自身业务逻辑

在验证签名正确并且result_code=SUCCESS情况下,商户做自身的业务逻辑,比如和DB的交互。

第三步:商户返回微信应答

如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)

上面是微信官方的一些说面,我们需要保证收到微信消息后做出相应应答,否则微信会一直通知我们,多次通知无应答后支付就失败了。

如何通知微信呢?用response告知微信

String result = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>";
response.getWriter().write(result);

当然,msg根据实际情况返回。下面列出整个流程的具体代码以及一些工具

整个流程code

@RequestMapping(value = "/payNotify")
	public void payNotify(HttpServletRequest request, HttpServletResponse response) throws IOException {
		logger.info("*****微信主动调用支付通知接口*****");
		// 微信会主动调用我们之前配置的notifyurl,并且以流的形式传输数据,首先从request中获得inputstream
		InputStream in = request.getInputStream();
		// 用工具类将inputstream转化成map集合
		Map<String, Object> resultMap = PayUtils.parseXml(in);
		logger.info(resultMap.toString());
		String result = "";
		// 需要进行签名验证,将所有的参数(除sign以外)签名后和传入的sign进行比对,如果正确才继续
		if (PayUtils.checkIsSignValidFromWechat((SortedMap<String, Object>) resultMap)) {
			// 信息处理
			String return_code = (String) resultMap.get("return_code");
			String result_code = (String) resultMap.get("result_code");
			// 由于微信后台会同时回调多次,所以需要做防止重复提交操作的判断
			// 成功后商户的业务逻辑
			if (Constant.RETURN_SUCCESS.equals(return_code) && Constant.RETURN_SUCCESS.equals(result_code)) {
				result = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
						+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>";
				//TODO 具体的商户逻辑
			} else {
				// FAIL的逻辑
				String err_code_des = (String) resultMap.get("err_code_des");
				logger.error("*****支付失败*****");
				result = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA["
						+ err_code_des + "]]></return_msg>" + "</xml>";
			}
		} else {
			// 签名失败的逻辑
			logger.error("*****签名验证错误*****");
			result = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
					+ "<return_msg><![CDATA[签名验证错误]]></return_msg>" + "</xml>";
		}
		// 通知微信.异步确认成功 不然微信会一直通知后台.八次之后就认为交易失败了.
		response.setCharacterEncoding("UTF-8");
		response.setContentType("text/xml");
		response.getWriter().write(result);
		response.getWriter().flush();
		response.getWriter().close();
	}

其中parseXml()方法上文有,就不再列出

检验数据中的签名是否合法

public static boolean checkIsSignValidFromWechat(SortedMap<String, Object> map) {
		String signFromWechat =(String)map.get("sign");
		if(isEmpty(signFromWechat)) {
			logger.info("*****微信返回的数据中签名不存在*****");
			return false;
		}
		//清除掉返回数据中的sign数据,因为sign本身是不参与签名的
		map.remove("sign");
		String signFromCreateSign=getSign(map);
		if(!signFromWechat.equals(signFromCreateSign)) {
			//签名验证不通过
			logger.info("*****签名验证不通过*****");
			return false;
		}
		//签名验证通过
		logger.info("*****签名验证通过*****");
		return true;
	}

到此为止,整个支付流程就完成啦~下面会继续讲退款

6.退款

官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4

和下单一样,需要调用官方API接口,下面先讲解字段:

appid==公众账号ID==同下单

mch_id==商户号==同下单

nonce_str==32位随机字符串==生成方法同下单

sign==签名==签名方法同同下单

sign_type==签名类型==同下单

out_trade_no==商户订单号==该退款单的单号

out_refund_no==商户退款单号==需要退款的单号

total_fee==订单金额==该退款订单的总价格

refund_fee==退款金额==需要退款的金额

所有参数赋值并签名后转化为xml封装好请求

https://api.mch.weixin.qq.com/secapi/pay/refund

还是使用之前的HttpUtil.Post(Constant.REFUND_URL,reqXml, true)方法,只不过退款需要加载证书,具体HttpUtil.Post()方法参考上文,下面列出里面加载证书的code

public static SSLConnectionSocketFactory initCert() throws Exception {
		FileInputStream instream = null;
		KeyStore keyStore = KeyStore.getInstance("PKCS12");
		instream = new FileInputStream(new File(Constant.CERT_PATH));
		keyStore.load(instream, Constant.MCH_ID.toCharArray());

		if (null != instream) {
			instream.close();
		}

		SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(keyStore, Constant.MCH_ID.toCharArray()).build();
		SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new String[] { "TLSv1" }, null,
				SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);

		return sslsf;
	}

其他代码同下单

之后通过返回的“return_code”以及“result_code”判断是否退款成功

注意:退款金额必须大于0,否则返回错误

整个微信公众号支付以及退款到此为止就结束了,如果开发时检查仔细,测试时是可以一遍通过的。如果出现问题,根据服务器后台log查看微信的一些返回值,和官方文档比对。大部分问题都是可以通过搜索引擎解决的~得意

猜你喜欢

转载自blog.csdn.net/tonywu1992/article/details/79388170