天天看點

微信支付(公衆号接入)-------------------------完整流程

花了幾天時間終于把微信支付做完了,雖然隻是調接口,但遇到的坑還是不少的,現在把整個流程梳理一下。

官方文檔位址: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備案。

微信開發目前來說還是比較順利的,就是需要各種配置有點麻煩。

繼續閱讀