微信公众号开发对接,开发文档也有蛮多坑,所以一路的血泪教训,这次先针对微信支付整理一下支付的踩坑全过程,开发时间紧现在整理出来,既是对此段时间的学习总结,也希望对遇到同样问题的童鞋可以有参考价值。
1. 微信扫码支付
认证微信服务号,申请开通微信支付功能,获取到微信支付商户号,然后可以进行开发。
扫码支付可分为两种模式,商户根据支付场景选择相应模式。(具体可以参考 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_3 流程比较详细)
模式一开发前,商户必须在公众平台后台设置支付回调URL。URL实现的功能:接收用户扫码后微信支付系统回调的productid和openid。先根据规则生成支付二维码,用户扫码获取openid,调用微信支付的统一下单接口生成预支付id,用户支付。
模式二流程相比模式一更为简单,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。注意:code_url有效期为2小时,过期后扫码不能再发起支付。
所以这次的扫码支付采用了简单点的模式二:
先调用统一下单接口:https://api.mch.weixin.qq.com/pay/unifiedorder
1) 参数:
private String appid = "";//微信支付分配的公众账号ID(企业号corpid即为此appId)
private String mch_id = "";//微信支付分配的商户号(申请通过之后邮件里收到的商户号)
private String nonce_str = "";//随机字符串,不长于32 位
private String sign = "";//根据API给的签名规则进行签名
private String body = "";//要支付的商品的描述信息
private String attach = "";//支付订单里面可以填的附加数据
private String out_trade_no = "";//商户系统内部的订单号
private int total_fee = 0;//订单总金额,单位为“分”,只能整数
private String spbill_create_ip = "";//订单生成的机器IP
private String time_start = "";//订单生成时间, 格式为yyyyMMddHHmmss
private String goods_tag = "";//商品标记,微信平台配置的商品标记,用于优惠券或者满减使用
private String trade_type = "NATIVE";//JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付,统一下单接口trade_type的传参可参考这里
private String notify_url = “”;//回调url 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
private String detail = "name";
private String openid = "";//JSAPI 一定要传 (下面的微信公众号支付一定要传的)
2) 签名生成算法:
public static String getSign(Map
ArrayList
for(Map.Entry
if(entry.getKey().equals("sign")) {
continue;
}
if (entry.getValue() != "") {
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String [] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for(int i = 0; i < size; i ++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + WxpayConfig.getKey();
result = MD5.MD5Encode(result).toUpperCase();
return result;
}
随机字符串生成
public static String getRandomStringByLength(int length) {// length<=32
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
3) 回调处理:
public String wxpaymentCallBack(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws Exception {
String returnXML = "";
try {
//获取HTTP请求的输入流
InputStream is = httpRequest.getInputStream();
//已HTTP请求输入流建立一个BufferedReader对象
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
//读取HTTP请求内容
String buffer = null;
StringBuffer sb = new StringBuffer();
while ((buffer = br.readLine()) != null) {
sb.append(buffer);
}
br.close();
ScanPayResDTO scanPayResDto = (ScanPayResDTO) Util.getObjectFromXML(sb.toString(), ScanPayResDTO.class);
System.out.println("微信回调:"+scanPayResDto.getReturn_code()+",订单号:"+scanPayResDto.getOut_trade_no());
//商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
if (scanPayResDto.getReturn_code().equals("SUCCESS")) {
if (scanPayResDto.getSign() != null && scanPayResDto.getOut_trade_no() != null) {
Map
System.out.println("微信返回的签名:"+scanPayResDto.getSign());
String sign = Signature.getSign(reqData);
System.out.println("系统生成的签名:"+sign);
if (scanPayResDto.getSign().equals(sign)) {//签名一样处理订单信息
logger.info("【支付成功】"); if(memberService.existOrderSnInRedis(scanPayResDto.getOut_trade_no())){
return returnXML("SUCCESS");
}
Charge charge = new Charge();
charge.setOrderNo(scanPayResDto.getOut_trade_no());//订单号
charge.setTransactionNo(scanPayResDto.getTransaction_id());//交易号
orderPaymentService.paymentCallBackAsyn(charge);
returnXML = returnXML("SUCCESS");
} else {
logger.info("【签名失败】");
returnXML = returnXML("FAIL");
}
}
} else {
returnXML = returnXML("FAIL");
logger.info("【支付失败】");
}
} catch (Exception e) {
returnXML = returnXML("FAIL");
}
return returnXML;
}
private String returnXML(String return_code) {
return "
+ "]]>
}
微信支付结果的通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒
注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
2. 微信公众号支付
公众号支付需要先在公众号设置支付目录(请确保实际支付时的请求目录与后台配置的目录一致,否则将无法成功唤起微信支付)和授权域名(开发公众号支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名)。设置参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3。
服务器端先调用统一下单接口,获取到prepay_id,再根据签名算法(直接调用上方扫码支付的签名算法)生成paySign(参与签名的参数有appId, timeStamp, nonceStr, package, signType,注意 大小写,时间戳要转成秒)
public Map
Map
map.put("appId", WxpayConfig.getAppid());
map.put("timeStamp", System.currentTimeMillis()/1000);
map.put("nonceStr", RandomStringGenerator.getRandomStringByLength(32));
map.put("package", "prepay_id=" + prepay_id);
map.put("signType", "MD5");
String sign = Signature.getSign(map);
map.put("paySign", sign);
return map;
}
注意:微信内置对象在其他浏览器中无效,即微信公众号支付只能在微信内部打开付款。
列表中参数名区分大小,大小写错误签名验证会失败。其中:低版本的timestamp字段名s小写,新版的timeStamp字段名S大写
低版本(见https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115):
wx.chooseWXPay({
timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: '', // 支付签名随机串,不长于 32 位
package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: '', // 支付签名
success: function (res) { // 支付成功后的回调函数
}
});
新版本(见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6):
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"wx2421b1c4370ec43b", //公众号名称,由商户传入
"timeStamp":"1395712654", //时间戳,自1970年以来的秒数
"nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串
"package":"prepay_id=u802345jgfjsdfgsdg888",
"signType":"MD5", //微信签名方式:
"paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名
},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ) {}
}
);
3. 微信支付--优惠券 红包支付--回调签名验证问题
还要注意一个问题 支付回调通知返回的数据最好用Map接收,再进行签名校验,因为如果有奖励金或者优惠券等参数,可能接收的对象没有写全而导致验签失败,踩坑教训,所以后面签名用Map了,微信官方jdk一开始也是对象,后面也改了,都是用的map接收,还有一个原因就是参数命名,如下图的参数名是不确定几个的,所以还是map最方便