微信小程序支付流程詳解

      網(wǎng)友投稿 1924 2025-04-01

      最近在工作中接入了一下微信小程序支付的功能,雖然說(shuō)官方文檔已經(jīng)比較詳細(xì)了,但在使用過(guò)程中還是踩了不少的坑,整理了一下大體的流程和代碼分享出來(lái)。在開(kāi)始使用小程序支付功能前,需要做好以下的準(zhǔn)備工作:

      申請(qǐng)微信小程序,配置小程序id及秘鑰

      申請(qǐng)用于支付的微信商戶平臺(tái)賬號(hào),配置商戶號(hào)id及商戶平臺(tái)秘鑰,并綁定小程序與該商戶號(hào)

      后端服務(wù)在正式環(huán)境下需要https域名,調(diào)試模式可以不需要

      先引用一張小程序支付官方說(shuō)明的流程圖,可以看出小程序支付的主要邏輯集中在后端,前端只需要攜帶參數(shù)請(qǐng)求后端接口,然后根據(jù)后端接口返回的數(shù)據(jù)在前端喚起微信支付即可。

      按照上面流程圖中商戶業(yè)務(wù)系統(tǒng)和微信支付系統(tǒng)主要交互步驟,對(duì)流程進(jìn)行拆解說(shuō)明。

      1、獲取用戶openId

      小程序前端調(diào)用wx.login()獲取登錄憑證code,后端調(diào)用接口獲取用戶的openid和session_key。注意這里在發(fā)起請(qǐng)求的時(shí)候需要攜帶小程序的appId和appSecret。

      public OpenIdInfo code2Openid(String code){ String url = "https://api.weixin.qq.com/sns/jscode2session"; String param = "appid=" + mpCommonProperty.getAppid() + "&secret=" + mpCommonProperty.getAppsecret() + "&js_code=" + code + "&grant_type=authorization_code"; String rs = HttpUtils.sendGet(url, param); JSONObject json = JSONObject.parseObject(rs); if (json.get("errcode") == null) { String openid = json.getString("openid"); String sessionKey = json.getString("session_key"); OpenIdInfo openIdInfo = OpenIdInfo.builder() .openId(openid).sessionKey(sessionKey).build(); return openIdInfo; }else { log.error("get openid error"); return null; } }

      需要注意每次調(diào)用接口都會(huì)刷新session_key的值,使之前的session_key失效,其他操作諸如解析用戶手機(jī)號(hào)時(shí)會(huì)用到這個(gè)秘鑰,為了避免該情況可以將用戶的openid存儲(chǔ)在業(yè)務(wù)系統(tǒng)的用戶體系中。

      2、調(diào)用支付統(tǒng)一下單

      微信統(tǒng)一下單接口要求傳遞參數(shù)的形式為xml報(bào)文,因此需要先對(duì)參數(shù)進(jìn)行拼接,這里僅列出了能夠喚起小程序支付所需要的最小參數(shù)范圍,更多的參數(shù)列表可以查看官方文檔。

      public String generateUniPayXml(UnifiedParam unifiedParam){ int money = (int) Math.ceil(unifiedParam.getTotalMoney() * 100); //轉(zhuǎn)換為分,向上取整 Map map=new HashMap<>(); map.put("appid", mpCommonProperty.getAppid()); //小程序id map.put("mch_id", mpCommonProperty.getMuchId()); //商戶號(hào) map.put("nonce_str",UUID.randomUUID().toString().replaceAll("-","")); //隨機(jī)字符串 map.put("body", unifiedParam.getPayBody()); //商品描述 map.put("out_trade_no", unifiedParam.getOrderNumber()); //商戶訂單號(hào) map.put("total_fee",String.valueOf(money)); //標(biāo)價(jià)金額, 訂單總金額單位為分 map.put("spbill_create_ip",IpUtils.getInternetIp()); //終端IP map.put("notify_url", mpCommonProperty.getServerDomain()+ "/pay/fallback");//通知地址 map.put("trade_type","JSAPI");//交易類型 map.put("openid", unifiedParam.getOpenid());//用戶標(biāo)識(shí),trade_type=JSAPI 時(shí)此參數(shù)必傳 String sign = signCommon(map); map.put("sign",sign); //生成簽名 String xml = XmlUtil.generateXmlFromMap(map); log.info(xml); return xml; }

      對(duì)其中幾個(gè)參數(shù)進(jìn)行說(shuō)明:

      微信小程序支付流程詳解

      out_trade_no:商戶訂單號(hào),在我們的后臺(tái)使用某種規(guī)則生成,不能重復(fù)

      total_fee:訂單總金額,需要注意單位為分,需要轉(zhuǎn)

      body:商品描述

      notify_url:支付結(jié)果的回調(diào)接口地址,使用會(huì)在后面介紹

      sign:簽名,需要按照微信的規(guī)則生成,算法規(guī)則為去除值為空的元素,參數(shù)名ASCII字典序排序進(jìn)行拼接,拼接API密鑰,使用Md5進(jìn)行加密:

      簽名方法如下:

      public String signCommon(Map map){ Set emptySet=new HashSet<>(); map.forEach((K,V)->{ if (StringUtils.isEmpty(map.get(K))){ emptySet.add(K); } }); for (String key : emptySet) { map.remove(key); } Set keySet = map.keySet(); String[] array = keySet.toArray(new String[keySet.size()]); Arrays.sort(array); StringBuffer sb=new StringBuffer(); for (String key : array) { sb.append(key+"="+map.get(key)+"&"); } sb.append("key=").append(mpCommonProperty.getMuchSecret()); System.out.println(sb.toString()); String md5Sign = Md5Utils.hash(sb.toString()); return md5Sign; }

      以上步驟完成后,對(duì)外暴露的統(tǒng)一下單接口如下:

      public Map unifiedOrder(UnifiedParam unifiedParam){ String xml = mpPayUtil.generateUniPayXml(unifiedParam); String url= "https://api.mch.weixin.qq.com/pay/unifiedorder"; String xmlResult = HttpUtils.sendPost(url, xml); //發(fā)送請(qǐng)求成功 if (xmlResult.indexOf("SUCCESS")!=-1){ Map parseXmlToMap = XmlUtil.parseXmlToMap(xmlResult); return parseXmlToMap; }else{ throw new RuntimeException("統(tǒng)一支付錯(cuò)誤"); } }

      在調(diào)用后,會(huì)收到同步返回結(jié)果為一段xml報(bào)文,將其解析成Map后可供下一階段使用,同步接口的返回值及錯(cuò)誤碼可以參考官方文檔。

      3、二次簽名

      在調(diào)用統(tǒng)一下單接口并收到微信的同步返回結(jié)果后,需要對(duì)其進(jìn)行二次簽名,需要進(jìn)行簽名的參數(shù)包括appId、timeStamp、nonceStr、package、signType。

      public PrepayInfo secondSign(Map unifiedOrderMap){ Map map=new HashMap<>(); map.put("appId", mpCommonProperty.getAppid()); map.put("timeStamp",String.valueOf(System.currentTimeMillis()/1000)); map.put("nonceStr",unifiedOrderMap.get("nonce_str")); map.put("package","prepay_id="+unifiedOrderMap.get("prepay_id")); map.put("signType",WechatConstants.signType); String sign = mpPayUtil.signCommon(map); map.put("paySign",sign); map.put("prePackage",unifiedOrderMap.get("prepay_id")); PrepayInfo prepayInfo =new PrepayInfo(); BeanUtil.copyProperties(map, prepayInfo); return prepayInfo; }

      二次簽名完成后,將timeStamp、nonceStr、package、signType、paySign返回給前端,這里為了方便封裝了一個(gè)對(duì)象用于返回,前端在收到參數(shù)后喚起微信支付。

      4、接收支付通知

      在前面介紹的統(tǒng)一下單的參數(shù)中,傳入了商戶后端的回調(diào)地址,在支付完成后,微信會(huì)向這個(gè)調(diào)用這個(gè)回調(diào)接口,通知支付結(jié)果。

      @PostMapping("fallback") public void fallback(HttpServletRequest request,HttpServletResponse response) throws IOException { StringBuilder sb = new StringBuilder(); BufferedReader reader = null; try (InputStream InputStream = request.getInputStream()) { reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); String line = ""; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { log.error("getBodyString錯(cuò)誤"); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { log.error(ExceptionUtils.getMessage(e)); } } } String notifyXml=sb.toString(); Map params = XmlUtil.parseXmlToMap(notifyXml); boolean result = false; String resultXml; if ("SUCCESS".equals(params.get("return_code"))) {//通信成功,非交易標(biāo)識(shí) //驗(yàn)證簽名 if (WechatPayUtil.validSignature(notifyXml)) { //執(zhí)行業(yè)務(wù)邏輯 result=true; }else{ log.error("微信支付成功回調(diào)驗(yàn)證簽名錯(cuò)誤!"); } }else { log.error("Fallback回調(diào)結(jié)果 : "+params.get("return_msg")); } if (result){ resultXml="" + ""; }else { resultXml="" + " "; } ServletOutputStream outputStream = response.getOutputStream(); outputStream.println(result); outputStream.close(); }

      在接收到返回的報(bào)文后,需要用之前同樣的簽名算法,驗(yàn)證返回報(bào)文的真實(shí)性,并在驗(yàn)證真實(shí)性后再執(zhí)行之后的業(yè)務(wù)邏輯,防止數(shù)據(jù)泄漏導(dǎo)致出現(xiàn)的虛假通知,造成資金損失。

      微信在調(diào)用回調(diào)接口時(shí),如果收到我們業(yè)務(wù)系統(tǒng)的應(yīng)答不符合規(guī)范或超時(shí),會(huì)判定本次通知失敗,重新發(fā)送多次通知。在通知一直不成功的情況下,按照官方文檔的說(shuō)明,總計(jì)在24h4m內(nèi)會(huì)調(diào)用15次回調(diào)接口。因此一定要按照規(guī)定返回成功接收的報(bào)文,從一定程度上也能降低系統(tǒng)的負(fù)載。

      在測(cè)試中發(fā)現(xiàn),不能使用直接返回String字符串的方式進(jìn)行結(jié)果的返回,仍然會(huì)一直發(fā)起回調(diào),必須使用HttpServletResponse寫(xiě)入返回。即使這么做了,還是建議大家在回調(diào)接口內(nèi)部處理業(yè)務(wù)前再做一下冪等性的處理,防止多次執(zhí)行回調(diào)邏輯造成業(yè)務(wù)系統(tǒng)的數(shù)據(jù)混亂。

      小程序

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:wps怎么截圖快捷鍵?
      下一篇:生產(chǎn)制造部安全管理制度(制造企業(yè)安全生產(chǎn)管理制度)
      相關(guān)文章
      亚洲色大成网站www永久一区| 亚洲成av人片一区二区三区| 亚洲午夜日韩高清一区| 亚洲av无码不卡久久| 久久亚洲精品无码| 久久久久久A亚洲欧洲AV冫| 亚洲不卡中文字幕| 亚洲福利电影一区二区?| 亚洲综合久久综合激情久久| 无码乱人伦一区二区亚洲一| 国产av无码专区亚洲av桃花庵| 精品久久久久久亚洲| 亚洲VA中文字幕不卡无码| 亚洲国产成人一区二区三区| 亚洲av永久无码精品表情包| 亚洲91av视频| 亚洲欧洲精品久久| 亚洲国产精品第一区二区三区| 亚洲福利视频一区二区| 337p日本欧洲亚洲大胆裸体艺术| 亚洲无线观看国产精品| 亚洲va国产va天堂va久久| 亚洲Av永久无码精品三区在线| 亚洲国产精久久久久久久| 亚洲精品日韩中文字幕久久久| 亚洲国产成人久久精品app| 午夜在线a亚洲v天堂网2019| 五月天网站亚洲小说| 亚洲av鲁丝一区二区三区| 久久久无码精品亚洲日韩蜜臀浪潮| 久久久亚洲裙底偷窥综合| 亚洲国产中文在线视频| 亚洲日韩一区二区一无码| 午夜亚洲国产理论片二级港台二级 | 亚洲美日韩Av中文字幕无码久久久妻妇| 亚洲精品国精品久久99热| 亚洲精品蜜桃久久久久久| 香蕉视频在线观看亚洲| 亚洲人成人77777在线播放| 亚洲日韩av无码中文| 亚洲女人被黑人巨大进入|