驗(yàn)簽解密)
簡(jiǎn)介這是一份面向Java開發(fā)者的H5微信支付接入實(shí)戰(zhàn)資料包主要解決電商網(wǎng)站、移動(dòng)端網(wǎng)頁等場(chǎng)景下的微信公眾號(hào)H5支付集成問題。內(nèi)容圍繞微信支付核心流程展開覆蓋官方接口文檔解讀、統(tǒng)一下單獲取prepay_id、支付鏈接與二維碼生成、H5頁面調(diào)起支付、異步回調(diào)驗(yàn)簽、訂單查詢兜底、異常重試與安全合規(guī)等要點(diǎn)同時(shí)也包含商戶號(hào)、商品描述、交易金額、回調(diào)URL等關(guān)鍵參數(shù)說明適合需要快速落地支付功能或理解支付閉環(huán)的初中級(jí)Java工程師。壓縮包共11個(gè)文件以5個(gè)Java源碼、3個(gè)XML配置、1個(gè)properties配置、1份部署必看doc和1個(gè)HTML測(cè)試頁面為主整體僅16KB結(jié)構(gòu)輕量且路徑清晰便于對(duì)照pom.xml與wxpay-test材料梳理依賴和調(diào)試環(huán)境。部署說明文檔記錄了測(cè)試與正式環(huán)境切換、參數(shù)配置、簽名生成等注意事項(xiàng)能幫助規(guī)避回調(diào)地址錯(cuò)誤、簽名失敗等常見坑HTML測(cè)試頁面則提供了模擬支付場(chǎng)景方便在接入階段快速驗(yàn)證流程。目前已有1320人學(xué)習(xí)下載是一份輕巧實(shí)用的H5支付對(duì)接參考。1. 從手機(jī)網(wǎng)頁到微信收銀臺(tái)這套 Java 微信支付 H5 鏈路到底長什么樣在手機(jī)瀏覽器里打開商城頁面點(diǎn)“微信支付”下一秒被帶進(jìn)微信收銀臺(tái)這個(gè)鏈路就是 H5 支付的核心動(dòng)作。很多 Java 后端會(huì)把微信內(nèi)置瀏覽器的公眾號(hào)支付和這個(gè)搞混結(jié)果同一套代碼在普通瀏覽器里死活拉不起收銀臺(tái)。區(qū)別在 User-Agent微信 App 內(nèi)的瀏覽器MicroMessenger走 JSAPI普通手機(jī)瀏覽器走 H5。我拆的這套 Java 微信支付 H5 資源包把服務(wù)端簽名、下單、回調(diào)驗(yàn)簽解密、查單對(duì)賬整條鏈路落成了可直接改的代碼適合正在做商城、預(yù)約系統(tǒng)、活動(dòng)頁的 Java 后端也適合外包交付時(shí)拿去當(dāng)?shù)鬃痈摹?. 接入前準(zhǔn)備四個(gè)密鑰參數(shù)與一條 UA 分流邏輯H5 支付接入前先把商戶平臺(tái)上的身份信息捋清楚。你后面每個(gè)請(qǐng)求簽名、每一條回調(diào)解密都依賴這幾個(gè)參數(shù)少一個(gè)請(qǐng)求都發(fā)不出去。這個(gè)環(huán)節(jié)不太能靠代碼調(diào)試解決配置錯(cuò)了就全鏈路返工。2.1 商戶平臺(tái)先備齊四個(gè)參數(shù)參數(shù)在哪拿作用商戶號(hào) mchid商戶平臺(tái)-賬戶中心-商戶信息標(biāo)識(shí)收款主體下單、查單都要帶服務(wù)號(hào) appid公眾平臺(tái)-開發(fā)-基本配置與商戶號(hào)綁定H5 支付的來源標(biāo)識(shí)APIv3 密鑰商戶平臺(tái)-API安全-APIv3密鑰32 字節(jié)對(duì)稱密鑰回調(diào)數(shù)據(jù)解密用商戶私鑰 API 證書序列號(hào)申請(qǐng) API 證書時(shí)生成的 apiclient_key.pem請(qǐng)求簽名私鑰序列號(hào)是簽名頭的一部分mchid 和 appid 是兩個(gè)體系的東西很多人混在一起。H5 支付里 appid 要用已完成微信認(rèn)證且與商戶號(hào)綁定的服務(wù)號(hào)個(gè)人主體、未認(rèn)證的服務(wù)號(hào)拉起收銀臺(tái)時(shí)會(huì)報(bào)錯(cuò)。APIv3 密鑰是 32 字節(jié)的字符串自己設(shè)置后要妥善保管回調(diào)解密就靠它丟了只能重置。申請(qǐng) API 證書時(shí)生成的商戶私鑰建議直接放到配置中心不要提交進(jìn)代碼倉庫。2.2 先分流User-Agent 決定走 H5 還是 JSAPI微信對(duì) H5 支付有約束在微信 App 內(nèi)置瀏覽器打開的頁面不允許直接拉起 H5 收銀臺(tái)強(qiáng)制走 JSAPI。所以業(yè)務(wù)入口處要做 UA 判斷別等到下單接口報(bào)了H5_PARAM_ERROR再回頭排查。// 入口處先分流微信內(nèi)置瀏覽器走JSAPI普通瀏覽器走H5支付 String userAgent request.getHeader(User-Agent); boolean isWeChatBrowser userAgent ! null userAgent.contains(MicroMessenger); if (isWeChatBrowser) { // 微信內(nèi)置瀏覽器走JSAPI用chooseWXPay調(diào)起 } else { // 普通手機(jī)瀏覽器走H5支付后端返回mweb_url給前端跳轉(zhuǎn) }這段代碼要放在生成支付方式的前置接口里不要只放在下單一處因?yàn)榍岸诵枰鶕?jù)這個(gè)結(jié)果渲染不同的支付按鈕。MicroMessenger是微信內(nèi)置瀏覽器的固定 UA 標(biāo)識(shí)幾乎不會(huì)變userAgent為 null 時(shí)默認(rèn)按非微信處理不要直接拋異常。如果你在內(nèi)網(wǎng)調(diào)試時(shí) UA 被瀏覽器插件改寫過這里可能判斷錯(cuò)排查時(shí)先打印日志看真實(shí) UA。2.3 手寫簽名還是直接上官方 SDK支付接入最大的阻力不是接口多而是簽名、驗(yàn)簽、解密這些環(huán)節(jié)散落在代碼里不好排查。我一般建議先手寫一遍簽名邏輯再上官方 SDK這樣 Header 里每個(gè)字段出錯(cuò)時(shí)都知道去哪查。手寫方式適合理解底層原理官方 SDK 適合生產(chǎn)環(huán)境快速交付。方式優(yōu)點(diǎn)缺點(diǎn)手寫簽名每個(gè)字段可控出錯(cuò)好排查平臺(tái)證書輪換要自己跟進(jìn)官方 SDK簽名驗(yàn)簽自動(dòng)處理出問題是個(gè)黑匣子只能看底層日志// 官方SDK方式配置一次后續(xù)請(qǐng)求自動(dòng)帶簽名 HttpClient httpClient WechatPayHttpClientBuilder.create() .withMerchant(mchId, serialNo, privateKey) .withValidator(new WechatPayValidator(platformCertVerifier)) .build(); // 后續(xù)通過該client發(fā)起的請(qǐng)求SDK會(huì)自動(dòng)計(jì)算Authorization頭并校驗(yàn)響應(yīng)上面這段是官方 SDK 的典型配置withMerchant里傳入商戶號(hào)、商戶證書序列號(hào)和商戶私鑰withValidator傳入平臺(tái)證書驗(yàn)證器。資源包里手寫簽名和 SDK 兩套實(shí)現(xiàn)都留了。生產(chǎn)環(huán)境我更建議用 SDK因?yàn)槠脚_(tái)證書更新時(shí) SDK 能跟著換手寫方案還要每月自己跑一遍證書下載腳本忘了就回調(diào)驗(yàn)簽失敗。3. 下單與拉起從構(gòu)造 JSON 到 mweb_url 跳轉(zhuǎn)H5 支付的核心動(dòng)作就兩個(gè)后臺(tái)下單拿 mweb_url前端跳轉(zhuǎn)進(jìn)收銀臺(tái)。下單接口的請(qǐng)求體結(jié)構(gòu)不復(fù)雜但每個(gè)字段都有講究尤其是場(chǎng)景信息少傳一個(gè)就報(bào)錯(cuò)。這一章把下單接口拆開講。3.1 H5 下單接口參數(shù)字段不多但每個(gè)都決定成敗H5 支付的下單接口是POST https://api.mch.weixin.qq.com/v3/pay/transactions/h5注意不是/v3/pay/transactions/jsapi很多人第一步就復(fù)制錯(cuò)。這個(gè)接口和 JSAPI 最大的區(qū)別是必須傳scene_info.h5_info場(chǎng)景信息這個(gè)字段是 H5 支付的身份證。參數(shù)必填說明appid是已綁定商戶號(hào)的服務(wù)號(hào) APPIDmchid是商戶號(hào)description是商品描述會(huì)展示在收銀臺(tái)out_trade_no是商戶訂單號(hào)必須唯一notify_url是回調(diào)地址公網(wǎng) HTTPSamount.total是總金額單位是分scene_info.h5_info是H5 專用場(chǎng)景信息redirect_url否支付完成后的回跳地址// 以fastjson為例構(gòu)造請(qǐng)求體金額單位是“分”int類型 int totalFee 990; // 9.90元由前端傳參換算后得到 JSONObject amount new JSONObject(); amount.put(total, totalFee); amount.put(currency, CNY); JSONObject h5Info new JSONObject(); h5Info.put(type, Wap); h5Info.put(wap_url, https://pay.shop.example.com); // 與支付授權(quán)域名一致 h5Info.put(wap_name, 模擬項(xiàng)目X); JSONObject sceneInfo new JSONObject(); sceneInfo.put(h5_info, h5Info); JSONObject body new JSONObject(); body.put(appid, appId); body.put(mchid, mchId); body.put(description, 測(cè)試商品-聯(lián)名款水杯); body.put(out_trade_no, orderNo); body.put(notify_url, https://pay.shop.example.com/api/pay/callback); body.put(amount, amount); body.put(scene_info, sceneInfo); body.put(redirect_url, https://shop.example.com/order/detail?orderNo orderNo);out_trade_no是商戶訂單號(hào)我習(xí)慣用“yyyyMMddHHmmss6位隨機(jī)數(shù)”拼避免同一天內(nèi)并發(fā)下單撞號(hào)。description會(huì)展示在用戶收銀臺(tái)頁面上寫清楚即可別塞下單時(shí)間之類的動(dòng)態(tài)內(nèi)容。h5_info里的wap_url是發(fā)起支付的網(wǎng)頁地址要求與商戶平臺(tái)配置的支付授權(quán)域名同域名否則報(bào)H5_PARAM_ERRORwap_name是這個(gè)網(wǎng)頁的名稱中文最多 6 個(gè)字。我把redirect_url放在此處一并傳用戶支付成功后回跳到此地址不傳默認(rèn)停在“支付成功”頁面。3.2 Authorization 頭四行簽名串和兩個(gè)易錯(cuò)點(diǎn)V3 接口的請(qǐng)求頭需要WECHATPAY2-SHA256-RSA2048簽名。簽名的原材料是請(qǐng)求方法、規(guī)范化 URL、時(shí)間戳、隨機(jī)串、請(qǐng)求體。這個(gè)簽名串格式是固定的五個(gè)部分之間用換行符分隔。String message httpMethod \n canonicalUrl \n timestamp \n nonce \n bodyStr \n; // 最后一個(gè)字段后面也要有換行符 Signature signer Signature.getInstance(SHA256withRSA); signer.initSign(privateKey); signer.update(message.getBytes(StandardCharsets.UTF_8)); String signature Base64.getEncoder().encodeToString(signer.sign()); String authHeader WECHATPAY2-SHA256-RSA2048 mchid\ mchId \, nonce_str\ nonce \, timestamp\ timestamp \, serial_no\ merchantSerialNo \, signature\ signature \;canonicalUrl對(duì) POST 就是去掉 query 的/v3/pay/transactions/h5對(duì) GET 查單要走/v3/pay/transactions/out-trade-no/{out_trade_no}?mchidxxx這種帶 query 的路徑query 要原樣拼進(jìn)去再參與簽名。這里有兩個(gè)高頻錯(cuò)誤一是serial_no用的是商戶 API 證書的序列號(hào)不是平臺(tái)證書序列號(hào)兩者搞混會(huì)報(bào)SIGN_ERROR二是簽名串每個(gè)字段間用\n換行最后一個(gè)bodyStr后面也要補(bǔ)一個(gè)\n這個(gè)換行符丟了微信返回驗(yàn)簽失敗。nonce每次請(qǐng)求都要隨機(jī)生成不能復(fù)用我一般用 UUID 去掉橫線取 32 位。3.3 mweb_url 跳轉(zhuǎn)與前端時(shí)序下單成功返回的 JSON 里核心字段是mweb_url。前端拿到后直接跳轉(zhuǎn)不要在中間再加一個(gè)自己的中轉(zhuǎn)頁那樣容易被風(fēng)控判定為異常調(diào)起。這塊前端代碼很短但時(shí)序處理決定體驗(yàn)。// 前端拿到后端返回的 mweb_url 后直接跳轉(zhuǎn) const res await fetch(/api/pay/h5/createOrder, { method: POST, body: JSON.stringify({ orderNo: currentOrderNo }) }); const { mwebUrl } await res.json(); window.location.href mwebUrl; // 微信收銀臺(tái)mweb_url有有效期用戶點(diǎn)了支付但停在前置頁面超過 2 小時(shí)再點(diǎn)跳轉(zhuǎn)會(huì)失效需要重新下單。前端這里最好加一個(gè)“支付生成中”的 loading 狀態(tài)避免重復(fù)點(diǎn)擊產(chǎn)生多個(gè)重復(fù)訂單。為了兼容微信 App 內(nèi)置瀏覽器打開的場(chǎng)景前端需要通過 UA 把 H5 和 JSAPI 入口分開這個(gè)判斷要和后端保持一致別前端跳了、后端又在下單時(shí)走 JSAPI 邏輯。另外下單接口返回的 HTTP 狀態(tài)碼要判斷200才解析 mweb_url4xx多半是參數(shù)問題5xx是微信側(cè)問題重試即可。4. 回調(diào)處理驗(yàn)簽、AES-256 解密與冪等更新支付結(jié)果微信不會(huì)同步返回而是異步 POST 到下單時(shí)填的 notify_url?;卣{(diào)處理是整條鏈路里最容易被輕視的一環(huán)但AEADBadTagException和重復(fù)通知這兩個(gè)問題幾乎每個(gè)項(xiàng)目都會(huì)遇到一次。這一章把驗(yàn)簽、解密和業(yè)務(wù)更新拆開講。4.1 回調(diào)頭里四個(gè)字段少驗(yàn)一個(gè)都是隱患回調(diào)請(qǐng)求頭里有四個(gè)關(guān)鍵字段Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature、Wechatpay-Serial。第一步驗(yàn)簽驗(yàn)的是簽名串timestamp \n nonce \n body \n和下單簽名的結(jié)構(gòu)一致但用的是平臺(tái)證書公鑰。String timestamp request.getHeader(Wechatpay-Timestamp); String nonce request.getHeader(Wechatpay-Nonce); String signature request.getHeader(Wechatpay-Signature); String serial request.getHeader(Wechatpay-Serial); String verifyMessage timestamp \n nonce \n bodyStr \n; PublicKey publicKey platformCertManager.getPublicKeyBySerial(serial); Signature verifier Signature.getInstance(SHA256withRSA); verifier.initVerify(publicKey); verifier.update(verifyMessage.getBytes(StandardCharsets.UTF_8)); boolean ok verifier.verify(Base64.getDecoder().decode(signature)); if (!ok) { // 驗(yàn)簽失敗返回非200狀態(tài)微信會(huì)稍后重試 response.setStatus(401); return; }platformCertManager用來管理平臺(tái)證書資源包里默認(rèn)放了一組證書生產(chǎn)上要對(duì)接證書下載接口定期更新證書更換后通過serial找到對(duì)應(yīng)公鑰。驗(yàn)簽失敗不要返回SUCCESS否則微信以為你處理成功了訂單狀態(tài)永遠(yuǎn)停在已支付未通知。常見驗(yàn)簽失敗原因是服務(wù)器時(shí)鐘漂移導(dǎo)致時(shí)間戳對(duì)不上或平臺(tái)證書換了你還沒更新。4.2 解密 resource 節(jié)點(diǎn)APIv3 密鑰與 AES-GCM 參數(shù)通知體里的數(shù)據(jù)是加密的密文放在resource.ciphertext里。解密算法固定是AES/GCM/NoPadding密鑰是 APIv3 密鑰這個(gè)環(huán)節(jié)的參數(shù)最容易取錯(cuò)。JSONObject resource body.getJSONObject(resource); String ciphertext resource.getString(ciphertext); String nonce resource.getString(nonce); String associatedData resource.getString(associated_data); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); GCMParameterSpec gcmSpec new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8)); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), AES), gcmSpec); cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8)); byte[] plain cipher.doFinal(Base64.getDecoder().decode(ciphertext)); JSONObject payData JSON.parseObject(new String(plain, StandardCharsets.UTF_8));這段代碼看起來簡(jiǎn)單但坑不少。apiV3Key是從商戶平臺(tái)設(shè)置的 32 字節(jié) APIv3 密鑰不是商戶私鑰也不是證書密碼nonce來自 resource 節(jié)點(diǎn)不是請(qǐng)求頭里那個(gè)Wechatpay-Nonce兩者不是一回事。解密后得到的 JSON 包含out_trade_no、transaction_id、trade_state和amount.total等字段其中trade_state為SUCCESS才表示支付成功。associated_data可能為空字符串但不能不傳。4.3 冪等更新與正確應(yīng)答微信回調(diào)會(huì)投遞多次所以回調(diào)處理必須冪等先查本地訂單狀態(tài)再?zèng)Q定要不要更新。順序反了訂單狀態(tài)就可能被后到的通知覆蓋回去。// 1. 先查本地訂單已成功則直接應(yīng)答SUCCESS OrderBo order orderService.getByOrderNo(payData.getString(out_trade_no)); if (order ! null TradeStatus.SUCCESS order.getStatus()) { return successResponse(response); // 已處理過直接應(yīng)答 } // 2. 校驗(yàn)金額用分比對(duì)避免精度問題 int paidTotal payData.getJSONObject(amount).getIntValue(total); if (paidTotal ! order.getTotalFee()) { response.setStatus(500); // 金額不一致讓微信重試 return; } // 3. 更新訂單為已支付并記錄transaction_id orderService.markPaid(order.getOrderNo(), payData.getString(transaction_id)); successResponse(response);先查再改是防重復(fù)回調(diào)最樸素也最可靠的做法。金額校驗(yàn)通過后再用事務(wù)把訂單標(biāo)記為已支付同時(shí)把微信交易單號(hào)transaction_id存下來對(duì)賬時(shí)要拿它和商戶平臺(tái)賬單核對(duì)。應(yīng)答成功時(shí)我返回HTTP 200body 為{code:SUCCESS,message:成功}如果你用空 body微信也認(rèn)但建議帶結(jié)構(gòu)方便日志排查。處理失敗返回HTTP 500微信會(huì)按自己的策略重試本地盡量保證接口冪等。5. H5 支付接入避坑五個(gè)真實(shí)翻車現(xiàn)場(chǎng)這五個(gè)坑是我前后接了好幾個(gè)商城項(xiàng)目攢下來的每個(gè)都是線上環(huán)境遇到的至少浪費(fèi)半天到一天不等。寫在這里照著排能少走幾段彎路。5.1 金額傳成了“元”賬單差點(diǎn)少一百倍現(xiàn)象用戶支付成功后對(duì)賬展示是 9.90 元微信賬單卻顯示 0.10 元。 原因amount.total的單位是分前端頁面展示習(xí)慣是元后端沒做轉(zhuǎn)換直接把元傳上去了。浮點(diǎn)數(shù)運(yùn)算是另一個(gè)隱患9.9 * 100在部分精度場(chǎng)景會(huì)得到 989.9999。 解決轉(zhuǎn)換用BigDecimalnew BigDecimal(9.90).movePointRight(2).intValue()不要在 double 上做乘法。5.2 scene_info 缺失下單直接報(bào) SCENE_INFO_ERROR現(xiàn)象下單接口返回SCENE_INFO_ERROR參數(shù)看著都對(duì)簽名也驗(yàn)過。 原因H5 支付接口要求scene_info.h5_info.type固定為Wap且wap_url域名要和商戶平臺(tái)配置的支付授權(quán)域名一致。少傳或域名對(duì)不上都會(huì)報(bào)這個(gè)錯(cuò)日志里不會(huì)提示具體哪個(gè)字段只能自己核對(duì)。 解決下單前把wap_url和redirect_url的域名打印到日志和商戶平臺(tái)的“支付授權(quán)域名”逐字符核對(duì)注意https和http算不同域名。5.3 回調(diào)解密報(bào) AEADBadTagException現(xiàn)象回調(diào)日志里出現(xiàn)AEADBadTagException: Tag mismatch解密出來亂碼或直接拋異常。 原因多半是nonce取錯(cuò)了地方——取了請(qǐng)求頭Wechatpay-Nonce而正確的是resource.nonce另一種是 APIv3 密鑰改過但服務(wù)沒重啟內(nèi)存里還是舊值。 解決換成resource.getString(nonce)改完密鑰后重啟服務(wù)。重放測(cè)試通知時(shí)用商戶平臺(tái)“交易中心-回調(diào)日志”里的原始報(bào)文最穩(wěn)妥。5.4 重復(fù)通知覆蓋了訂單狀態(tài)現(xiàn)象一筆訂單收到兩次回調(diào)第二次把訂單狀態(tài)從“已支付”覆蓋回“待支付”。 原因代碼里沒有先查本地訂單而是直接執(zhí)行“根據(jù)回調(diào)更新狀態(tài)”后到的通知把狀態(tài)又改了。 解決嚴(yán)格按先查再改的順序等于已支付狀態(tài)下直接應(yīng)答不做二次更新。這個(gè)順序不要圖省事去掉。5.5 用戶點(diǎn)支付后停留在“無法拉起”頁面現(xiàn)象能下單、能拿到 mweb_url但用戶跳轉(zhuǎn)后看到報(bào)錯(cuò)無法進(jìn)入收銀臺(tái)。 原因appid 未與商戶號(hào)完成綁定或該 appid 對(duì)應(yīng)的服務(wù)號(hào)主體和商戶主體不一致也可能支付授權(quán)目錄沒配。這個(gè)問題的奇怪之處在于下單階段不報(bào)錯(cuò)等拉起時(shí)才暴露。 解決到商戶平臺(tái)“產(chǎn)品中心”檢查 AppID 綁定關(guān)系綁定需要公眾號(hào)管理員掃碼確認(rèn)同時(shí)確認(rèn)服務(wù)號(hào)已完成微信認(rèn)證“支付授權(quán)目錄”和“支付授權(quán)域名”都指向?qū)嶋H域名。6. 落地驗(yàn)證三板斧curl 復(fù)現(xiàn)、查單兜底、對(duì)賬單核對(duì)接入完成后最怕的不是代碼跑不起來而是線上出問題沒法復(fù)現(xiàn)。我習(xí)慣在本地把下單請(qǐng)求原樣打出來用 curl 重放再用查單接口做兜底最后用對(duì)賬單收尾。這套流程跑通了交付才踏實(shí)。6.1 用 curl 復(fù)現(xiàn)下單請(qǐng)求curl --location https://api.mch.weixin.qq.com/v3/pay/transactions/h5 \ --header Content-Type: application/json \ --header Accept: application/json \ --header Authorization: WECHATPAY2-SHA256-RSA2048 mchid1900000001,nonce_str... \ --data {appid:wx...,mchid:1900000001,description:測(cè)試商品,out_trade_no:20250101001,notify_url:https://pay.shop.example.com/api/pay/callback,amount:{total:1,currency:CNY},scene_info:{h5_info:{type:Wap,wap_url:https://pay.shop.example.com,wap_name:測(cè)試}}}Authorization 里的簽名串需要從服務(wù)端日志里把完整值抄過來不要自己拼拼錯(cuò)反而干擾排查。用這個(gè)命令可以復(fù)現(xiàn)簽名、參數(shù)、域名任何一環(huán)的問題本地改完參數(shù)再發(fā)起響應(yīng)結(jié)果立刻能看出來。6.2 查單接口兜底回調(diào)丟了也不慌回調(diào)偶爾會(huì)延遲或丟接通查單接口是標(biāo)配。查單地址是GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchidxxx這個(gè) GET 請(qǐng)求照樣要簽名注意規(guī)范化 URL 要帶 query。String query mchid mchId; String canonicalUrl /v3/pay/transactions/out-trade-no/ orderNo ? query; String message GET\n canonicalUrl \n timestamp \n nonce \n \n; // 請(qǐng)求頭帶上與下單時(shí)相同的Authorization結(jié)構(gòu)GET 的規(guī)范化 URL 要把 query 帶進(jìn)簽名串這和 POST 不同。查單返回的trade_state如果為SUCCESS就走和回調(diào)里一樣的訂單更新邏輯。我一般把查單做成定時(shí)補(bǔ)償任務(wù)對(duì)創(chuàng)建超過 5 分鐘仍處于待支付的訂單發(fā)起查單這樣即使回調(diào)沒到訂單狀態(tài)也能收斂。6.3 對(duì)賬單上線前最后一道核對(duì)上線前我會(huì)讓測(cè)試環(huán)境先跑幾筆一分錢交易然后在商戶平臺(tái)下載當(dāng)日對(duì)賬單逐筆核對(duì)out_trade_no、transaction_id、金額三者一致。這套動(dòng)作和功能測(cè)試無關(guān)純粹是驗(yàn)證加密、簽名、域名配置在整個(gè)鏈路上沒有殘留問題。從那以后我每次接支付類項(xiàng)目都強(qiáng)制自己先走一遍“小額交易-查單-退款”閉環(huán)再交付代碼希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取