一.小程序支付
参考小程序支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_4&index=3
账号支持:小程序appid,小程序secret,商户号mchid,商户secret
服务端和微信支付系统主要交互:
1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】
前端调用接口wx.login() 获取临时登录凭证(code)
通过code值获取openid地址:
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
请求参数:
参数 必填 说明
appid 是 小程序唯一标识
secret 是 小程序的 app secret
js_code 是 登录时获取的 code
grant_type 是 填写为 authorization_code
发送一个get请求即可参数也比较容易理解,这样我们就完成了第一步获取到了用户的openid
2、商户server调用支付统一下单,api参见公共api【统一下单API】
统一下单接口地址:
https://api.mch.weixin.qq.com/pay/unifiedorder
提供代码片段:
/**
* 商户发起生成预付单请求(统一下单接口)
*
* @param body 商品信息
* @param orderId 商户自己的订单号
* @param totalFee 支付价格(单位分)
* @param openid 微信用户的openid(必须关注此公众号的openid)
* @param notifyUrl 微信回调地址
*/
public Map unifiedOrder(String body, Long orderId, Long totalFee, String openid, String notifyUrl) {
//微信账号校验
checkConfig(weixinProperties);
SortedMap params = new TreeMap<>();
//小程序appid
params.put("appid", "**************");
//商户号
params.put("mch_id", "**************");
params.put("spbill_create_ip", getUserContext().getIp());
params.put("trade_type", "JSAPI";
params.put("nonce_str", StringUtil.getRandomStringByLength(24));
params.put("notify_url", notifyUrl);
params.put("body", body);
params.put("out_trade_no", fictitiousOrderService.insertFictitiousOrder(orderId, body, totalFee).toString());
params.put("total_fee", totalFee);
params.put("openid", openid);
//签名算法
String sign = getSign(params);
params.put("sign", sign);
String xml = XmlHelper.mapToXml(params);
try {
//发送xmlPost请求
String xmlStr = HttpUtil.xmlPost("https://api.mch.weixin.qq.com/pay/unifiedorder", xml);
//加入微信支付日志
payWechatLogService.insertPayWechatLog(Constants.PAY_UNIFIED_ORDER_RESULT_LOG, xmlStr);
Map map = XmlHelper.xmlToMap(xmlStr);
if (map != null && Constants.REQUEST_SUCCESS.equals(map.get("result_code")) && map.get("prepay_id") != null) {
//返回二次签名前端调用微信信息
Map result = secondarySign(map);
return result;
} else {
//异常通知
mqSendService.sendRobotMsg(
String.format("微信消息-传入参数[%s];微信输出[%s]", JSON.toJSONString(params), JSON.toJSONString(map)),
"统一下单");
throw serviceExceptionService.createServiceException(ExceptionConstants.UNIFIED_ORDER_INTERFACE_ERROR);
}
} catch (Exception e) {
mqSendService.sendRobotMsg(String.format("微信消息-[%s]", e.getMessage()), "统一下单");
//统一下单接口异常
throw serviceExceptionService.createServiceException(ExceptionConstants.UNIFIED_ORDER_INTERFACE_ERROR);
}
}
方法注意点:
参数out_trade_no商户订单号即自己数据库存储的订单号这里有个坑,统一下单接口不予许同一订单发起两次下单请求,但是我们业务常常有这个需要。解决的办法是下单时用商户订单号生成一个虚拟订单号,用虚拟支付单号去请求微信,微信回调时用返回的虚拟支付单号解析出商户的订单号,修改订单的支付状态。
微信下单和退款的接口请求方式都是将参数转为xml格式发送跑
/**
* 获取签名 md5加密(微信支付必须用MD5加密) 获取支付签名
*/
public String getSign(SortedMap params) {
StringBuffer sb = new StringBuffer();
//所有参与传参的参数按照accsii排序(升序)
Set es = params.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
//key后面接商户号的秘钥secret
sb.append("key=" + "*************");
String sign = EncryptUtil.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
return sign;
}
签名规则:
所有的参数按照accsii排序(升序),
所有参数按照key1=value1&key2=value2&…拼接成字符串
最后在key后面拼上秘钥(注:该秘钥是商户秘钥不是小程序秘钥)
通过MD5加密字符串后将所有的字符改为大写
微信在线签名校验工具地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1
注:微信下单和退款的接口请求方式都是将参数转为xml格式发送post请求,提供xmlPost方法代码
/**
* 微信提交xml参数
*/
public static String xmlPost(String url1, String xml) {
try {
// 创建url资源
URL url = new URL(url1);
// 建立http连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 设置允许输出
conn.setDoOutput(true);
conn.setDoInput(true);
// 设置不用缓存
conn.setUseCaches(false);
// 设置传递方式
conn.setRequestMethod("POST");
// 设置维持长连接
conn.setRequestProperty("Connection", "Keep-Alive");
// 设置文件字符集:
conn.setRequestProperty("Charset", "UTF-8");
//转换为字节数组
byte[] data = xml.getBytes();
// 设置文件长度
conn.setRequestProperty("Content-Length", String.valueOf(data.length));
// 设置文件类型:
conn.setRequestProperty("contentType", "text/xml");
// 开始连接请求
conn.connect();
OutputStream out = conn.getOutputStream();
// 写入请求的字符串
out.write(data);
out.flush();
out.close();
System.out.println(conn.getResponseCode());
// 请求返回的状态
if (conn.getResponseCode() == 200) {
System.out.println("连接成功");
// 请求返回的数据
InputStream in = conn.getInputStream();
String a = null;
try {
byte[] data1 = new byte[in.available()];
in.read(data1);
// 转成字符串
a = new String(data1);
System.out.println(a);
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
return a;
} else {
System.out.println("no++");
}
} catch (Exception e) {
}
return null;
}
3、商户server调用再次签名,api参见公共api【再次签名】
通过统一下单接口我们可以拿到prepay_id,将prepay_id组装成package进行签名。
签名参数:
字段名 变量名 必填 类型 示例值 描述
小程序ID appId 是 String wxd678efh567hg6787 微信分配的小程序ID
时间戳 timeStamp 是 String 1490840662 时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
随机串 nonceStr 是 String 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,不长于32位。推荐随机数生成算法
数据包 package 是 String prepay_id=wx2017033010242291fcfe0db70013231072 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=wx2017033010242291fcfe0db70013231072
签名方式 signType 是 String MD5 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
/**
* 二次签名
*/
private Map secondarySign(Map map) {
SortedMap secondarySignParam = new TreeMap<>();
secondarySignParam.put("appId", weixinProperties.getMiniapp().getUser().getAppId());
secondarySignParam.put("timeStamp", new Date().getTime() + "");
secondarySignParam.put("nonceStr", StringUtil.getRandomStringByLength(24));
secondarySignParam.put("package", "prepay_id=" + map.get("prepay_id").toString());
secondarySignParam.put("signType", "MD5");
String paySign = getSign(secondarySignParam);
secondarySignParam.put("paySign", paySign);
//签完名去掉appId防止暴露账号
secondarySignParam.remove("appId");
return secondarySignParam;
}
前端调起微信支付参数:
参数 类型 必填 说明
timeStamp String 是 时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
nonceStr String 是 随机字符串,长度为32个字符以下。
package String 是 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
signType String 是 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
paySign String 是 签名,具体签名方案参见微信公众号支付帮助文档;
组装前端所需要的参数通过统一下单接口直接返回给前端,前端调用wx.requestPayment(OBJECT)发起微信支付。
4、商户server接收支付通知,api参见公共api【支付结果通知API】
接口地址为【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。
/**
* 微信支付回调
*/
@RequestMapping(value = "/notify", produces = "application/xml; charset=UTF-8")
@ResponseBody
public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
BufferedReader reader = request.getReader();
StringBuffer inputString = new StringBuffer();
String line = "";
while ((line = reader.readLine()) != null) {
inputString.append(line);
}
payWechatLogService.insertPayWechatLog(Constants.PAY_SUCCESS_RESULT_LOG, inputString.toString());
Map map = XmlHelper.xmlToMap(inputString.toString());
String return_code = map.get("return_code");
//客户订单id(虚拟支付单号)
String out_trade_no = map.get("out_trade_no");
//微信支付订单号(流水号)
String transaction_id = map.get("transaction_id");
//todo 修改订单对应的状态
//商户处理后同步返回给微信参数
response.getWriter().print(" ");
}
回调成功给微信发送通知,不然微信会继续回调该接口
二.微信退款
参考小程序支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4
账号支持:小程序appid,商户号mchid,商户secret,退款证书,退款证书密码
系统中有了支付肯定会设及到退款
申请退款接口地址:https://api.mch.weixin.qq.com/secapi/pay/refund(请求需要双向证书。 详见证书使用)
实现代码:
/**
* 申请退款
*
* @param orderId 商户订单号
* @param refundId 商户退款单号
* @param totalFee 订单金额
* @param refundFee 退款金额
* @param refundAccount 退款资金来源(默认传 "REFUND_SOURCE_UNSETTLED_FUNDS")
*/
public Map refund(Long orderId, String refundId, Long totalFee,
Long refundFee, String refundAccount) {
checkConfig(weixinProperties);
SortedMap params = new TreeMap<>();
params.put("appid", "************");
params.put("mch_id", "************");
params.put("nonce_str", StringUtil.getRandomStringByLength(24));
//商户订单号和微信订单号二选一
params.put("out_trade_no", fictitiousOrderService.findFictitiousIdByOrder(orderId));
params.put("out_refund_no", refundId);
params.put("total_fee", totalFee);
params.put("refund_fee", refundFee);
params.put("refund_account", refundAccount);
//签名算法
String sign = getSign(params);
params.put("sign", sign);
try {
String xml = XmlHelper.mapToXml(params);
String xmlStr = doRefund("https://api.mch.weixin.qq.com/secapi/pay/refund", xml);
//加入微信支付日志
payWechatLogService.insertPayWechatLog(Constants.PAY_REFUND_RESULT_LOG, xmlStr);
Map map = XmlHelper.xmlToMap(xmlStr);
if (map == null || !Constants.REQUEST_SUCCESS.equals(map.get("result_code"))) {
//消息通知
mqSendService.sendRobotMsg(
String.format("微信消息-传入参数[%s];微信输出[%s]", JSON.toJSONString(params), JSON.toJSONString(map)),
"申请退款");
}
//未结算金额不足 使用余额退款
if (map != null && Constants.REQUEST_FAIL.equals(map.get("result_code")) &&
Constants.REQUEST_SUCCESS.equals(map.get("return_code")) && Constants.REFUND_NOT_ENOUGH_MONEY.equals(map.get("err_code")) &&
Constants.REFUND_SOURCE_UNSETTLED_FUNDS.equals(refundAccount)) {
refund(orderId, refundId, totalFee, refundFee, Constants.REFUND_SOURCE_RECHARGE_FUNDS);
}
return map;
} catch (Exception e) {
//微信退款接口异常
mqSendService.sendRobotMsg(String.format("微信消息-传入参数[%s];异常信息-[%s]", JSON.toJSONString(params), e.getMessage()), "申请退款");
throw serviceExceptionService.createServiceException(ExceptionConstants.PAY_REFUND_INTERFACE_RRROR);
}
}
方法注意点:
其中getSign(params)获取签名方法和上面支付签名方法一致可共用;out_trade_no填写微信支付时对应的虚拟支付单号;这里我根据refund_account退款资金来源作了一个逻辑处理,退款资金优先退商户未结算资金,如果未结算资金不足退商户余额的资金。
退款请求及证书的使用:
/**
* 申请退款
*/
public String doRefund(String url, String data) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
FileInputStream is = new FileInputStream(new File("****证书文件存放的路劲*******"));
try {
keyStore.load(is, "********证书密码*******".toCharArray());
} finally {
is.close();
}
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(
keyStore,
"********证书密码*******".toCharArray())
.build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,
new String[]{"TLSv1"},
null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
);
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
try {
HttpPost httpost = new HttpPost(url); // 设置响应头信息
httpost.addHeader("Connection", "keep-alive");
httpost.addHeader("Accept", "*/*");
httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
httpost.addHeader("Host", "api.mch.weixin.qq.com");
httpost.addHeader("X-Requested-With", "XMLHttpRequest");
httpost.addHeader("Cache-Control", "max-age=0");
httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
httpost.setEntity(new StringEntity(data, "UTF-8"));
CloseableHttpResponse response = httpclient.execute(httpost);
try {
HttpEntity entity = response.getEntity();
String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
EntityUtils.consume(entity);
return jsonStr;
} finally {
response.close();
}
} finally {
httpclient.close();
}
}
证书密码如果没设置默认为商户号mchid