开发之前翻阅了很多帖子,结合自己的实际开发情况,将微信支付/退款 流程以及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服务器为例:
修改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 jsonToEntity(String jsonString, Class 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 buildParamMap(PaySendData data) {
SortedMap paramters = new TreeMap();
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 map) {
StringBuffer buffer = new StringBuffer();
Set> set = map.entrySet();
Iterator> iterator = set.iterator();
while (iterator.hasNext()) {
Map.Entry 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 parseXml(InputStream inputStream) {
SortedMap map = new TreeMap();
try {
// 获取request输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素所有节点
List 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
现在将这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 = "" + " "+ " " + " ";
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 resultMap = PayUtils.parseXml(in);
logger.info(resultMap.toString());
String result = "";
// 需要进行签名验证,将所有的参数(除sign以外)签名后和传入的sign进行比对,如果正确才继续
if (PayUtils.checkIsSignValidFromWechat((SortedMap) 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 = "" + " "
+ " " + " ";
//TODO 具体的商户逻辑
} else {
// FAIL的逻辑
String err_code_des = (String) resultMap.get("err_code_des");
logger.error("*****支付失败*****");
result = "" + " " + " " + " ";
}
} else {
// 签名失败的逻辑
logger.error("*****签名验证错误*****");
result = "" + " "
+ " " + " ";
}
// 通知微信.异步确认成功 不然微信会一直通知后台.八次之后就认为交易失败了.
response.setCharacterEncoding("UTF-8");
response.setContentType("text/xml");
response.getWriter().write(result);
response.getWriter().flush();
response.getWriter().close();
}
其中parseXml()方法上文有,就不再列出
检验数据中的签名是否合法
public static boolean checkIsSignValidFromWechat(SortedMap 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查看微信的一些返回值,和官方文档比对。大部分问题都是可以通过搜索引擎解决的~