花了几天时间终于把微信支付做完了,虽然只是调接口,但遇到的坑还是不少的,现在把整个流程梳理一下。
官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1
大概流程:后台调用统一下单接口,获取pre_pay_id ------> 将获取到的这个预支付标示和一些其他参数返回给前端 --------
----------> 前端接收参数利用微信浏览器内置对象(或者js api的发起支付接口)发出支付请求 --------------------->支付成功,微信服务器通知后台,后台处理相关业务逻辑。
下面上代码:
一、获取调用统一下单接口,获取pre_pay_id.
/**
* 微信统一下单
* @param userIp 客户端ip
* @param totalFee 费用
* @param order 订单
* @param openId 用户openId
* @return prepay_id 预支付交易会话标识
* @throws Exception
*/
public static Map<String,String> unifiedorder(String userIp, String totalFee,
String order, String openId)throws Exception{
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
Map<String,String> requestMap = unifiedorderTemplate(userIp, totalFee, order, openId);
String requestStr = WXPayUtil.mapToXml(requestMap);
// String requestStr = new String(WXPayUtil.mapToXml(requestMap).getBytes(),"utf-8");
log.info("微信统一下单请求地址:" + url);
log.info("微信统一下单请求参数:" + requestStr);
String result = HttpUtil.sendXmlPost(url, requestStr);
log.info("微信统一下单请求结果:" + result);
Map<String,String> resultMap = WXPayUtil.xmlToMap(result);
if(resultMap.containsKey("return_code")){
if(resultMap.get("return_code").equals("SUCCESS")){
if(resultMap.containsKey("result_code")){
if(resultMap.get("result_code").equals("SUCCESS")){
return resultMap; //请求并且下单成功返回的参数里有pre_pay_id和一些其他参数,具体看官方文档
}
}
}
}
return null;
}
/**
* 统一下单参数封装
* @param userIp
* @param totalFee
* @param order
* @param openId
* @return
* @throws Exception
*/
private static Map<String,String> unifiedorderTemplate(String userIp, String totalFee,
String order, String openId)throws Exception{
Map<String,String> requestMap = new HashMap<>();
requestMap.put("appid", WxConstants.APP_ID);
requestMap.put("mch_id", WxConstants.MCH_ID);
requestMap.put("nonce_str", WXPayUtil.generateNonceStr());
String body = "中文";
requestMap.put("body", body);
requestMap.put("out_trade_no",order);
requestMap.put("total_fee", totalFee);
// requestMap.put("spbill_create_ip", "118.114.230.35");
requestMap.put("spbill_create_ip", userIp);
requestMap.put("notify_url", WxConstants.NOTIFY_URL);
requestMap.put("trade_type", "JSAPI");
requestMap.put("openid", openId);
requestMap.put("sign", WXPayUtil.generateSignature(requestMap, WxConstants.KEY));
/*String xml = "<xml>" + "<appid><![CDATA[" + WxConstants.APP_ID + "]]></appid>"
+ "<body><![CDATA[" + body + "]]></body>"
+ "<mch_id><![CDATA[" +WxConstants.MCH_ID + "]]></mch_id>"
+ "<nonce_str><![CDATA[" + requestMap.get("nonce_str") + "]]></nonce_str>"
+ "<notify_url><![CDATA[" + requestMap.get("notify_url") + "]]></notify_url>"
+ "<out_trade_no><![CDATA[" + requestMap.get("out_trade_no") + "]]></out_trade_no>"
+ "<spbill_create_ip><![CDATA[" + requestMap.get("spbill_create_ip") + "]]></spbill_create_ip>"
+ "<total_fee><![CDATA[" + totalFee + "]]></total_fee>"
+ "<trade_type><![CDATA[" + "JSAPI" + "]]></trade_type>"
+ "<openid><![CDATA[" + openId + "]]></openid>"
+ "<sign><![CDATA[" + requestMap.get("sign") + "]]></sign>" + "</xml>";
requestMap.put("xml", xml);*/
return requestMap;
}
有几点需要注意:
1.参数较多,参数不能缺失,注意appid,mch_id,key的正确,这里的key在进行参数签名时会用到,是在商户平台设置的支付密钥,与公众号的appsecret不是同一个。
2.注意传值编码问题,如果签名错误,但是去官方提供的签名验证工具验证是正确的,那多半是设置的key不对或者传的参数里有中文发生乱码了。
二、将获取到的pre_pay_id和一些其他参数返回给前端。
@Override
public ReturnObject createOrder(String ip, String openId, String fee) throws Exception {
ReturnObject ro = new ReturnObject();
VipOrder order = new VipOrder();
order.setFee(Integer.parseInt(fee));
order.setOdrdeTime(System.currentTimeMillis());
order.setOpenId(openId);
order.setOutTradeNo(CommonUtil.makeOrderNumber(System.currentTimeMillis()));
Map<String, String> resultMap = WxPay.unifiedorder(ip, fee, order.getOutTradeNo(), openId);
if (resultMap != null) {
String prePayId = resultMap.get("prepay_id");
if (prePayId == null || prePayId.equals("")) {
ro.setMessage("无法获取pre_pay_id");
} else {
order.setPrepayId(prePayId);
orderDao.insertSelective(order);
Map<String, String> jsMap = new HashMap<>();
jsMap.put("appId", resultMap.get("appid"));
jsMap.put("timeStamp", new Long(System.currentTimeMillis() / 1000).toString());
jsMap.put("nonceStr", WXPayUtil.generateNonceStr());
jsMap.put("package", "prepay_id=" + prePayId);
jsMap.put("signType", "MD5");
String paySign = WXPayUtil.generateSignature(jsMap, WxConstants.KEY);
jsMap.put("paySign", paySign);
log.info("返回前端:" + jsMap.toString());
ro.setData(jsMap);
}
}
return ro;
}
这里只需要注意一点,因为调用统一下单成功后返回的参数里也有nonceStr,sign,timeStamp这些参数,我一开始以为就是把这些返回给前端,结果并没有关系,而且签名是根据js api调用需要的参数重新计算的,这里参与签名的有appId,timeStamp,nonceStr,package,signType计算出的,签名算法与统一下单签名算法一样,而且好像签名类型要一致,不过我没试,我签名都是采用的MD5。
三、微信js唤出支付框
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<button value="充值" id = "test">充值</button>
<span th:text="${session.app}"></span>
</body>
<script type="text/javascript" src="/static/js/jquery-3.1.1.js"></script>
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
<script type="text/javascript" th:inline="javascript">
var a = [[${session.sign}]];
alert(a);
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: [[${session.sign.appId}]], // 必填,公众号的唯一标识
timestamp: [[${session.sign.timestamp}]], // 必填,生成签名的时间戳
nonceStr: [[${session.sign.nonceStr}]], // 必填,生成签名的随机串
signature: [[${session.sign.signature}]],// 必填,签名,见附录1
jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表,这里只写支付的
});
wx.ready(function(){
wx.hideOptionMenu();//隐藏右边的一些菜单
});
$("#test").click(function(){
var openId = JSON.stringify("o9b1k0Yov9IRnX_MAB4suLydMvgA");
$.ajax({
contentType:"application/json",
dataType:"json",
type:"POST",
url:"****",
data:{
"openId":openId
},
success:function(result){
console.log(result);
if(result.success == true){
// pay(result.data);
onBridgeReady(result.data);
}
}
})
})
/* function pay(json){
wx.chooseWXPay({
timestamp: json.timeStamp,
nonceStr: json.nonceStr,
package: json.package,
signType: 'MD5',
paySign: json.paySign,
success: function (res) {
alert("支付成功");
}
});
} */
function onBridgeReady(param){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"", //公众号名称,由商户传入
"timeStamp":param.timeStamp, //时间戳,自1970年以来的秒数
"nonceStr":param.nonceStr, //随机串
"package":param.package,
"signType":"MD5", //微信签名方式:
"paySign":param.paySign //微信签名
},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
alert("支付成功");
} // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
}
);
}
/* 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();
} */
</script>
</html>
两种方式都试过,都可以成功唤出支付框,但是用js api调用接口的方式要麻烦一点,需要获取js_ticket,是调用接口用token获得的,然而和需要调用的url进行签名,然后返回前端进行wxConfig配置,如果只是支付的话用微信内置对象唤起支付就方便的多。
到这里就不得不吐槽一句了。作为一个很少做前端的后端狗,就是这里浪费了我至少开发微信支付的一半时间,难受。
四、处理支付回调
/**
* 微信支付通知
* @param req
* @param rep
*/
@RequestMapping(value = "/handlePayNotify", method = RequestMethod.POST)
public void handlePayNotify(HttpServletRequest req, HttpServletResponse rep){
try {
req.setCharacterEncoding("UTF-8");
rep.setCharacterEncoding("UTF-8");
Map<String, String> map = XmlUtils.praseXml(req);
wxService.handlePayNotify(req, rep, map);
} catch (Exception e) {
log.error(e.getMessage(),e);
}
}
@Override
public synchronized void handlePayNotify(HttpServletRequest req, HttpServletResponse rep, Map<String, String> map)
throws Exception {
Map<String, String> returnMap = new HashMap<String, String>();
String return_code = WxConstants.FAIL;
String return_msg = "";
if (map.containsKey("return_code") && map.containsKey("result_code") && map.get("return_code").equals("SUCCESS")
&& map.get("result_code").equals("SUCCESS")) {
// 验证签名
String sign = map.get("sign");
String mySign = WXPayUtil.generateSignature(map, WxConstants.KEY);
if (!sign.equals(mySign)) {
log.error("支付通知签名验证失败");
log.error("sign:" + sign);
log.error("mySign" + mySign);
return_msg = "签名验证失败";
}
// 验证订单支付状态
String outTradeNo = map.get("out_trade_no");
VipOrder order = orderService.selectVipOrderByOutTradeNo(outTradeNo);
if (order.getPayStatus() == 1) {
return_code = WxConstants.SUCCESS;
} else {
// 验证金额
String wxTotalFee = map.get("total_fee");
if (order.getFee().toString().equals(wxTotalFee)) {
order.setActualFee(Integer.parseInt(map.get("cash_fee")));
order.setPayStatus((byte) 1);
order.setPayTime(DateUtils.dateStrToTimestamp(map.get("time_end"), DateUtils.DATE_TYPE_4));
order.setTransactionId(map.get("transaction_id"));
if (orderDao.updateByPrimaryKeySelective(order) == 1) {
// 添加会员记录
VipRecord record = orderService.addVipRecord(order.getOpenId());
if (record != null) {
return_code = WxConstants.SUCCESS;
}
}
} else {
log.error("支付金额和订单金额不一致");
return_msg = "支付金额与订单金额不一致";
}
}
returnMap.put("return_code", return_code);
returnMap.put("return_msg", return_msg);
String returnStr = WXPayUtil.mapToXml(returnMap);
rep.getWriter().write(returnStr);
}
}
这里的回调处理接口对应统一下单传的url参数。微信官方推荐针对通知为了安全最好验证签名与金额,并且要能正确处理微信的重复通知。下面是微信官方原话:
支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。
对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。微信支付基本完成。额外提醒一个在设置支付授权目录时,设置的url应该是你调用微信支付的上一级,微信文档那里说的必须细化至二三级表述的有点问题。另外这是公众号支付,需要获取用户的openId,需要配置网页授权域名,域名必须经过ipc备案。
微信开发目前来说还是比较顺利的,就是需要各种配置有点麻烦。