花了幾天時間終于把微信支付做完了,雖然隻是調接口,但遇到的坑還是不少的,現在把整個流程梳理一下。
官方文檔位址: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備案。
微信開發目前來說還是比較順利的,就是需要各種配置有點麻煩。