前段时间在公司开发微信支付,心里真是一万头草泥马奔腾而过,微信支付里面的坑有多少,真的只有走过人的人才知道,这里我就说一下我用过的微信支付的三个功能,一是扫描二维码支付,二是公众号支付(也就是Js调起支付),三是申请微信退款。
一 :扫码支付
扫码支付分为两种模式,模式二稍微简单一点,我们公司用的就是模式二,扫码支付的模式二说白了就是下微信的统一订单接口,然后获得一个支付url,不管是通过草料二维码生成器还是用xzing代码生成一个二维码亦或是在微信里直接点击这个url,都是可以调起微信支付的。记住,只有在微信里点击这个url才有效,也只有通过微信的二维码扫描去扫描这个支付二维码才有效。
调微信的统一下单接口说白了就是去访问那个接口,把所有统一下单接口需要的数据包成xml格式访问过去,微信回给我们的数据也是xml格式,解析一下就行了
我将微信统一下单接口所需要用到的数据封装了一下
public class WData {
private String nonceStr;
private String sign;
private String body;
private String orderNo;
private int orderTotal = 0;
public WData(String body, String orderNo, Integer orderTotal) {
this.body = body;
this.orderNo = orderNo;
String currTime = DateUtil.date2String(new Date(), "yyyyMMddHHmmss");
nonceStr = currTime.substring(8, currTime.length()) + RandomUtil.buildRandom(4);
this.orderTotal = orderTotal;
SortedMap<String, String> packageParams = new TreeMap<String, String>();
packageParams.put("appid", Constants.ACCESS_KEY_ID);
packageParams.put("body", body);
packageParams.put("mch_id", Constants.MCH_ID);
packageParams.put("nonce_str", nonceStr);
packageParams.put("notify_url", Constants.NOTIFY_URL);
packageParams.put("out_trade_no", orderNo);
packageParams.put("spbill_create_ip", Constants.SPBILL_CREATE_IP);
packageParams.put("total_fee", orderTotal + "");
packageParams.put("trade_type", Constants.TRADE_TYPE);
sign = cresteSign(packageParams);
}
private String cresteSign(SortedMap<String, String> packageParams) {
StringBuffer sb = new StringBuffer();
Set<?> es = packageParams.entrySet();
Iterator<?> it = es.iterator();
while (it.hasNext()) {
@SuppressWarnings("rawtypes")
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + Constants.PARTNER_KEY);
return MD5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
}
@Override
public String toString() {
// TODO 只有必填参数,非必填参数后续补充
StringBuffer result = new StringBuffer();
result.append("<xml><appid>").append(Constants.ACCESS_KEY_ID).append("</appid>")
.append("<mch_id>").append(Constants.MCH_ID).append("</mch_id>")
.append("<nonce_str>").append(nonceStr).append("</nonce_str>")
.append("<sign>").append(sign).append("</sign>")
.append("<body><![CDATA[").append(body).append("]]></body>")
.append("<out_trade_no>").append(orderNo).append("</out_trade_no>")
.append("<total_fee>").append(orderTotal).append("</total_fee>")
.append("<spbill_create_ip>").append(Constants.SPBILL_CREATE_IP).append("</spbill_create_ip>")
.append("<notify_url>").append(Constants.NOTIFY_URL).append("</notify_url>")
.append("<trade_type>").append(Constants.TRADE_TYPE).append("</trade_type></xml>");
return result.toString();
}
}
这里toString是我们返回给微信的xml包,里面是微信统一下单所需要的全部数据,这里有很多需要注意的地方。
appid,mch_id是商户信息,这个我就不说了,找公司架构师或者技术总监去要吧。
nonce_str是一个随机字符串,貌似有长度规定,看一下微信文档,别太长或太短就行了。
body是显示字样,当用户调起微信支付的时候,出现在他手机上显示的商品名称。
out_trade_no是订单编号,一定要入自己的数据库,日后很多操作都必不可少的需要这玩意。
total_fee是订单的价格,单位为分。
spbill_create_ip是自个服务器的Ip地址,记住,这个值在公众号支付的时候就是用户的Ip地址,别弄错了。
notify_url是回调地址,这个就是微信支付第一个坑,首先这块功能你在自己本机是测试不了的,因为必须得放在服务器上微信才能访问的到,其次这个地址必须是可以直接访问而不能带参数,你说坑不坑,坑不坑,简直天坑。当用户把钱给付了,微信就会访问这个接口,把一些数据也是用xml形式给带过来,xml包里面有订单编号,这样我们就能判断是哪一个订单的钱到账了,还有就是如果你用的框架是spring mvc框架的话,这个地址精确到某一个controller就行了,不要精确到某个准确地址,我们公司用的spring boot框架,就是只要精确到回调的controller就行了。
trade_type是支付方式,我们这种支付方式就是NATIVE。
sign是签名,一般情况下你只要数据没错,用的函数没错,这玩意不会错的。//这块容易出错的点就是必须得按照字母顺序先排序再进行加密。
调取微信统一下单接口,可以获取一个xml,通过如下代码解析
public static Map doXMLParse(String strxml) throws Exception {
if (null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = String2Inputstream(strxml);
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 = getChildrenText(children);
}
m.put(k, v);
}
// 关闭流
in.close();
return m;
}
/**
* 获取子结点的xml
*
* @param children
* @return String
*/
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(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
public static InputStream String2Inputstream(String str) {
return new ByteArrayInputStream(str.getBytes());
}
获取到一个map,直接通过
String code_url=(String) map.get("code_url");
获取到支付url,当然了,这里面还有很多有用的数据,不过我们关心的也就是这个支付url,可以通过草料二维码或者xzing代码生成二维码
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
直接在pom文件里面把这段代码拷贝进去,添加xzing的jar包。
接下来的操作就是用户扫二维码,付钱,然后呢,我们咋知道人家付钱了,而且是付的哪个订单呢,就是notify_url该派上用场的时候了,前面说了这个url里面是不可以写参数的,所以是通过读流的方式读取请求,然后解析出一个xml来,所以说,真!的!很!坑!
//微信回调接口
@ResponseBody
@RequestMapping(value="/back", method = RequestMethod.POST)
public void back(HttpServletRequest request, HttpServletResponse response) {
System.out.println("微信访问");
InputStream stream=null;
String s;
try {
stream=request.getInputStream();
s=getString(stream);
} catch (IOException e1) {
e1.printStackTrace();
s=null;
}
}
private String getString(InputStream stream) throws IOException{
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line + "\n");
}
stream.close();
return sb.toString();
}
这里的controller我就没贴全部的代码了,那个String s就是一个xml,再按照上述的方式将其解析出来就好了
二 :公众号支付(JS支付)
之所以这种支付方式叫做公众号支付而不叫JS支付,是因为只有在微信公众号里面才有可能调的起来这种支付,不是说用微信浏览器直接打开某个网页就能调起支付,有些人可能会问微信浏览器直接打开某个网页和在公众号内部打开网页有什么区别,区别就是直接用微信浏览器打开网页而不和公众号关联的话是获取不到用户的openId的,而获取不到用户的openId,调起公众号支付就别想了。以我的理解是,扫码支付之所以有比公众号支付更大的自由性,是因为公众号支付更像是关上门做自家生意,但是微信不允许你在外面用着我的支付系统做生意,你只能把店开在我微信公众号的内部。
依旧是先调取统一支付接口,但是这次的数据和扫码支付有些许不同
public class WData {
private String nonceStr;
private String sign;
private String body;
private String orderNo;
private int orderTotal = 0;
private String ip;
private String openid;
public WData(String ip,String body, String orderNo, Integer orderTotal,String openid) {
this.body = body;
this.orderNo = orderNo;
this.ip=ip;
this.openid=openid;
String currTime = DateUtil.date2String(new Date(), "yyyyMMddHHmmss");
nonceStr = currTime.substring(8, currTime.length()) + RandomUtil.buildRandom(4);
this.orderTotal = orderTotal;
SortedMap<String, String> packageParams = new TreeMap<String, String>();
packageParams.put("appid", Constants.ACCESS_KEY_ID);
packageParams.put("body", body);
packageParams.put("mch_id", Constants.MCH_ID);
packageParams.put("nonce_str", nonceStr);
packageParams.put("notify_url", Constants.NOTIFY_URL);
packageParams.put("openid",openid);
packageParams.put("out_trade_no", orderNo);
packageParams.put("spbill_create_ip", ip);
packageParams.put("total_fee", orderTotal + "");
packageParams.put("trade_type", Constants.JSAPI_TRADE);
sign = cresteSign(packageParams);
}
private String cresteSign(SortedMap<String, String> packageParams) {
StringBuffer sb = new StringBuffer();
Set<?> es = packageParams.entrySet();
Iterator<?> it = es.iterator();
while (it.hasNext()) {
@SuppressWarnings("rawtypes")
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + Constants.PARTNER_KEY);
return MD5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
}
@Override
public String toString() {
// TODO 只有必填参数,非必填参数后续补充
StringBuffer result = new StringBuffer();
result.append("<xml><appid>").append(Constants.ACCESS_KEY_ID).append("</appid>")
.append("<mch_id>").append(Constants.MCH_ID).append("</mch_id>")
.append("<nonce_str>").append(nonceStr).append("</nonce_str>")
.append("<sign>").append(sign).append("</sign>")
.append("<body><![CDATA[").append(body).append("]]></body>")
.append("<openid>").append(openid).append("</openid>")
.append("<out_trade_no>").append(orderNo).append("</out_trade_no>")
.append("<total_fee>").append(orderTotal).append("</total_fee>")
.append("<spbill_create_ip>").append(ip).append("</spbill_create_ip>")
.append("<notify_url>").append(Constants.NOTIFY_URL).append("</notify_url>")
.append("<trade_type>").append(Constants.JSAPI_TRADE).append("</trade_type></xml>");
return result.toString();
}
}
不同之处有三点:1,多了个openid,这是微信用户在某个公众号的唯一标识,获取这玩意又是一段血泪史。2,spbill_create_ip不再是商户服务器地址,而是用户的Ip地址。3,trade_type的值改成JSAPI
这样获取到一个xml,按照上面的代码解析xml,这次咱不是获取code_url,而是prepay_id,这个是接下来在网页用Js调起微信支付所需要的关键数据
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(back);
}
function onBridgeReady(back){
var appId=back.appId;
var timeStamp=back.timeStamp;
var nonceStr=back.nonceStr;
var packa=back.packa;
var signType=back.signType;
var paySign=back.paySign;
WeixinJSBridge.invoke(
'getBrandWCPayRequest',{
"appId":appId,
"timeStamp":timeStamp,
"nonceStr" :nonceStr,
"package" :packa,
"signType" :signType,
"paySign" : paySign
},function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
alert('支付成功');
} // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
else{
alert('支付失败');
}
});
}
这样咱把数据传到前端,稍微解释一下各个数据
timeStamp是时间戳,1970年到现在的秒数。
signType是加密方式,我这是MD5。
package在这里写我们获取到的prepay_id,不过格式是
“prepay_id=”+prepay_id
这里其余的数据在前面的统一支付接口都是有的 ,就不说了。
公众号支付的最后,吐槽一下微信的天坑,首先,这块的命名跟前面的不一样,而且不止这一处,好多地方的命名,其实是一个意思,但是命名有区别,有些是大小写,有些是短杠,你说坑不坑。
还有,最tm tm坑的一点,这块的js代码是我从微信文档中直接复制黏贴的,先开始一直崩溃,我以为是没有暴露在外层服务器才出现的缘故,结果再一检查,握草,竟然是里面有中文字符//因为我用的不是什么html编辑器,而是直接在eclipse写的,所以中文字符也没报错。
三:申请退款
申请退款这块有点跟前面不同,这块如果发送请求,就必须得用微信给商户的证书进行发送,不然请求直接无效,这是我封装的请求代码
private String payHttps(String url,String data) throws Exception {
//商户id
String MCH_ID = "";
//指定读取证书格式为PKCS12
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// String path =PropUtil.getPropertyValue("wx.sz.certificate.path", "D:/apiclient_cert.p12").replace("\"","");
//读取本机存放的PKCS12证书文件
FileInputStream instream = new FileInputStream(new File("D:/apiclient_cert.p12"));
try {
//指定PKCS12的密码(商户ID)
keyStore.load(instream, MCH_ID.toCharArray());
} finally {
instream.close();
}
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, MCH_ID.toCharArray()).build();
//指定TLS版本
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,new String[] { "TLSv1" },null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
//设置httpclient的SSLSocketFactory
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");
System.out.println("微信回调"+jsonStr);
EntityUtils.consume(entity);
return jsonStr;
} finally {
response.close();
}
} finally {
httpclient.close();
}
}
apiclient_cert.p12是微信给商户的证书,它放置的地方跟代码中保持一致,比如我放在了d盘,代码里就是
FileInputStream instream = new FileInputStream(new File(“D:/apiclient_cert.p12”));
这里的url就是微信的退款接口
https://api.mch.weixin.qq.com/secapi/pay/refund
data则是访问退款接口所需要的xml包
这里所需要用到的数据给展示一下
"<xml>"+
"<appid>"+appid+"</appid>"+
"<mch_id>"+mch_id+"</mch_id>"+
"<nonce_str>"+nonce_str+"</nonce_str>"+
"<sign>"+sign+"</sign>"+
"<out_trade_no>"+order.getNo()+"</out_trade_no>"+
"<out_refund_no>"+refund.getCode()+"</out_refund_no>"+
"<total_fee>"+order.getTotal()+"</total_fee>"+
"<refund_fee>"+refund.getRefundFee()+"</refund_fee>"+
"<op_user_id>"+op_user_id+"</op_user_id>"+
"<transaction_id>"+"</transaction_id>"+
"</xml>";
就说一下上面没用到的数据
out_refund_no是退款编号,一定要入库,方便日后管理;
total_fee是订单的价格,这个数据是不能错的;
refund_fee是退款金额,这个金额是不能大于订单价格的;
op_user_id填商户Id就行了,我也不记得干嘛的了;
访问微信,提交完了退款申请之后,微信还是会返回一个xml包,里面有个return_code字段,这个字段不是说微信有没有退款,而是你退款申请有没有成功,一般情况下这个字段都是SUCCESS。
就像上面说的,我们只能确定我们的申请有没有成功,但是没办法确定钱究竟退没退怎么办,微信提供了一个接口给我们进行查询,我的做法是写了个定时任务,把所有状态为申请成功但是不确定退没退的退款申请拿去访问退款查询接口,如果退款成功,就把状态改掉,这里再封装一个类
public class WRefund {
private String out_refund_no;
private String nonceStr;
private String sign;
public WRefund(String out_refund_no){
this.out_refund_no=out_refund_no;
String currTime = DateUtil.date2String(new Date(), "yyyyMMddHHmmss");
this.nonceStr = currTime.substring(8, currTime.length()) + buildRandom(4);
SortedMap<String, String> packageParams = new TreeMap<String, String>();
packageParams.put("appid", Constants.ACCESS_KEY_ID);
packageParams.put("mch_id", Constants.MCH_ID);
packageParams.put("nonce_str", nonceStr);
packageParams.put("out_refund_no", out_refund_no);
this.sign=cresteSign(packageParams);
}
private String cresteSign(SortedMap<String, String> packageParams) {
StringBuffer sb = new StringBuffer();
Set<?> es = packageParams.entrySet();
Iterator<?> it = es.iterator();
while (it.hasNext()) {
@SuppressWarnings("rawtypes")
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=Kk7Y0sKJcHnUiOXeiZcc5J0qfH62y8ZE");
return MD5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
}
private int buildRandom(int length) {
int num = 1;
double random = Math.random();
if (random < 0.1) {
random = random + 0.1;
}
for (int i = 0; i < length; i++) {
num = num * 10;
}
return (int) ((random * num));
}
@Override
public String toString() {
// TODO 只有必填参数,非必填参数后续补充
StringBuffer result = new StringBuffer();
result.append("<xml><appid>").append(Constants.ACCESS_KEY_ID).append("</appid>")
.append("<mch_id>").append(Constants.MCH_ID).append("</mch_id>")
.append("<nonce_str>").append(nonceStr).append("</nonce_str>")
.append("<sign>").append(sign).append("</sign>")
.append("<out_refund_no>").append(out_refund_no).append("</out_refund_no></xml>");
return result.toString();
}
}
很好理解了,里面就只有一些基本数据和退款编号,访问微信的退款查询接口,如果微信返回的xml包里的result_code值为SUCCESS,则表明这笔退款的钱已经退了。
尾记:反正开发微信后端各种心累,像我这样的萌萌哒小程序员,又没权限直接把程序放在服务器上,有好多东西都没办法测试,只能把代码写好,提交到git上,我们架构师把代码拖下来,跑起来测试,经常代码盲交,然后出了什么bug,只能自己猜哪里有问题。而且微信开发真的真的各种坑!!!!