尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微信JSAPI支付实战:从下单到退款对账的完整接口封装与踩坑记录

微信JSAPI支付实战:从下单到退款对账的完整接口封装与踩坑记录 简介微信JSAPI支付Demo面向Java后端开发者用于快速接入微信公众号/服务号JSAPI支付并完整整合关闭订单、查询订单、申请退款、查询退款、下载对账单等高频商户接口帮助解决支付回调、订单状态同步与售后对账等常见需求。资源压缩包共498个文件主要包含Java源码、JSP页面、XML与properties配置、jar依赖库以及p12商户证书等包体约23.09MB目录结构清晰适合直接导入开发环境参照调试。已有946人学习下载可作为理解微信支付接口编排与异常处理流程的入门材料。通过该Demo可掌握统一下单、支付回调验签、主动查单、关单、退款与账单下载的核心实现证书加载和配置项均有示例便于结合商户平台实测。1. 微信JSAPI支付Demo的整体设计与接口梳理接到一个商城项目要把支付模块从支付宝切到微信JSAPI支付。我原本以为这就是调一个下单接口、拉起收银台的事真正做完才发现订单取消、支付结果不同步、用户申请退款、财务对账哪一样都绕不开微信支付后台接口。最后我整理出的这套Demo包含关闭订单、查询订单、查询退款、下载对账单、申请退款外加最核心的JSAPI下单与调起支付基本覆盖了商城业务里的全部支付场景。1.1 六个接口分别解决什么问题老规矩先理清楚每个接口的业务位置不然代码写着写着就乱套。JSAPI支付和扫码支付不太一样它发生在微信内置浏览器里用户点完支付按钮后整个交互链条是“前端拉起支付 - 微信收银台 - 回调通知 - 主动查单”。这中间任何一个环节断了都需要靠后台接口去补位。接口触发场景需要注意的点JSAPI下单用户点“微信支付”后端拿到用户openid后创建预付单返回prepay_id还要二次签名才能调起支付查询订单回调没收到、客户端断网、状态需要主动校对根据out_trade_no查尽量在轮询任务里用上关闭订单用户主动取消未支付订单或超时未支付已支付订单不能关闭要转退款申请退款用户申请退款、运营手动退款、订单部分退款需要商户证书双向认证退款金额不能超过原订单查询退款退款状态异步变化需要同步给用户状态有PROCESSING、SUCCESS、CLOSED、ABNORMAL下载对账单财务每天拉账单做核对接口返回的是下载链接要再请求一次才能真正拿到内容我把这六个接口放在同一个Demo里不是简单堆接口文档而是希望形成一个闭环前端拉起支付后端自己查状态订单取消了能关单要退钱了能退还能对账。比如用户支付时一直没收到回调你不能让用户干等着而是要根据订单号主动查询再比如用户发起了退款但退款需要银行处理客户端必须从查询退款接口拿到最终状态才能展示给用户。有了这套东西商城的支付模块才算是完整落地。1.2 Demo的技术选型与目录结构这次我用PHP写后端没有直接用官方SDK而是基于APIv3手写了HTTP请求封装。原因很简单SDK虽然方便但很多同学出了问题后根本不理解签名和证书的机制换个语言或者换个场景就抓瞎。手写一遍整个链路就通了。运行环境是PHP 8 cURL扩展不需要额外的Composer依赖这也是这个Demo最方便的地方拉到服务器上配置好路径就能跑。目录结构大概这样wechatpay-jsapi-demo/ ├── config.php # 商户号、AppID、证书路径等配置 ├── WechatPayClient.php # 请求封装、签名、证书加载 ├── pay.php # JSAPI下单、生成调起参数 ├── order.php # 查询订单、关闭订单 ├── refund.php # 申请退款、查询退款 ├── bill.php # 下载对账单 └── callback.php # 支付/退款回调处理每个文件只做一件事方便后面单独调试。接下来才是重头戏签名和证书。2. 动手前的硬核准备密钥、证书与签名很多人在微信支付对接上翻车原因不是接口逻辑难而是没搞明白APIv3的鉴权机制。我先把这些基础概念讲透。2.1 APIv3密钥、商户私钥和证书序列号新版微信支付APIv3和老版v2的MD5签名完全不一样。它用的是非对称加密签名同时资金类接口还要求商户证书双向认证。需要准备三样东西商户私钥、证书序列号、APIv3密钥。商户私钥就是商户平台下载的apiclient_key.pem只用来做请求签名和后续解密回调绝不能上传到前端。证书序列号与商户证书对应的一串数字用来告诉微信“我是哪个证书签的名”不是私钥内容也不是证书文件里那串可见的Base64文本。APIv3密钥32位字符串主要用在回调通知解密上和商户号密码不是一回事。配置在config.php里return [ mchid 你的商户号, appid 公众号AppID, api_v3_key 32位APIv3密钥, serial_no 商户证书序列号, private_key_path /path/to/apiclient_key.pem, cert_path /path/to/apiclient_cert.pem, ];资金类接口如申请退款需要双向证书所以cURL里还要用到apiclient_cert.pem和私钥这两步不能漏。我见过一个同学在本地测试时把证书路径写成了相对路径程序跑起来正常部署到Linux服务器上就报“证书文件不存在”最后排查半天才发现是路径解析问题。建议所有证书路径都用绝对路径或者在入口文件里统一define。2.2 签名与请求头的实现微信APIv3要求每个请求的Authorization头里带上签名。签名串格式是固定的HTTP请求方法\n URL路径\n 请求时间戳\n 请求随机串\n 请求报文body\n注意最后有个换行body如果是GET请求就是空字符串。URL路径只包含path和query不包含域名。比如查询订单的路径是/v3/pay/transactions/out-trade-no/订单号?mchid商户号。我封装了一个统一的请求方法签名逻辑写在这里private function buildAuth(string $method, string $pathWithQuery, string $body ): array { $timestamp time(); $nonce bin2hex(random_bytes(16)); $message $method . \n . $pathWithQuery . \n . $timestamp . \n . $nonce . \n . $body . \n; openssl_sign($message, $rawSign, $this-privateKey, OPENSSL_ALGO_SHA256); $signature base64_encode($rawSign); $auth sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,timestamp%d,serial_no%s,signature%s, $this-mchid, $nonce, $timestamp, $this-serialNo, $signature ); return [ Authorization $auth, Content-Type application/json, Accept application/json, ]; }这里random_bytes是为了保证随机串唯一性。实际开发中我见过有人直接把随机串写死结果微信直接报签名错误。签名不对时微信通常返回401别急着怀疑接口地址先打印一下自己拼出来的message和文档样例对比十有八九是换行或路径多了空格。3. 核心接口实现从下单到退款、对账单接下来我把Demo里每个核心接口的落地过程拆开讲。代码不是完整粘贴但关键参数和调用方式都做了注释照着改就能跑。3.1 JSAPI下单与前端调起支付下单接口路径是POST /v3/pay/transactions/jsapi。请求体里最重要三个东西appid、mchid、payer.openid。openid是用户关注公众号或授权登录后拿到的标识JSAPI支付必须有它不需要用户授权手机号。public function jsapiPay(string $openid, string $outTradeNo, int $total, string $desc) { $path /v3/pay/transactions/jsapi; $body [ appid $this-appid, mchid $this-mchid, description $desc, out_trade_no $outTradeNo, notify_url https://api.example.com/callback.php, amount [ total $total, currency CNY ], payer [ openid $openid ] ]; $result $this-request(POST, $path, json_encode($body, JSON_UNESCAPED_UNICODE)); return $result[prepay_id]; }拿到prepay_id后前端还不能直接调起需要后端再做一次签名生成这样的参数$params [ appId $this-appid, timeStamp (string) time(), nonceStr bin2hex(random_bytes(16)), package prepay_id . $prepayId, signType RSA, ]; $message $params[appId] . \n . $params[timeStamp] . \n . $params[nonceStr] . \n . $params[package] . \n; openssl_sign($message, $rawSign, $this-privateKey, OPENSSL_ALGO_SHA256); $params[paySign] base64_encode($rawSign);前端拿到这些参数后在微信内置浏览器里这样调用WeixinJSBridge.invoke(getBrandWCPayRequest, params, function (res) { if (res.err_msg get_brand_wcpay_request:ok) { // 支付成功后端以回调为准 } });如果是小程序里调起需要把WeixinJSBridge.invoke换成wx.requestPayment参数基本一致。支付成功后不要立刻给用户发货要以服务端回调收到的结果为准。我见过太多只依赖前端回调做业务更新的情况一刷新页面就数据错乱。3.2 查询订单与关闭订单查询订单对应GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchid{mchid}。这个接口主要用来解决回调丢失时主动对账。我建议在订单支付等待队列里每隔一段时间查一次查到SUCCESS就更新库。public function queryOrder(string $outTradeNo) { $path /v3/pay/transactions/out-trade-no/ . $outTradeNo . ?mchid . $this-mchid; $result $this-request(GET, $path); return $result[trade_state]; // SUCCESS / NOTPAY / CLOSED / REVOKED / PAYERROR }关闭订单是POST /v3/pay/transactions/out-trade-no/{out_trade_no}/close请求体只有mchid。注意关闭订单只对“未支付”和“支付中”的订单有效。如果订单已经支付成功调用关闭接口会报ORDERPAID这种场景要走退款流程而不是关单。public function closeOrder(string $outTradeNo) { $path /v3/pay/transactions/out-trade-no/ . $outTradeNo . /close?mchid . $this-mchid; $body [mchid $this-mchid]; $this-request(POST, $path, json_encode($body)); }这里有个细节关闭订单的路径和查询订单不一样查询订单的mchid放在query参数里而关闭订单的路径里也要带上?mchid同时body里再传一次mchid。之前有同事漏掉query参数微信直接返回路径错误。3.3 申请退款与查询退款申请退款是资金类操作必须使用商户证书做双向认证。我在request()方法里支持了一个withCert参数发起退款时传入truepublic function refund(string $outTradeNo, string $outRefundNo, int $refundAmount, int $totalAmount, string $reason) { $path /v3/refund/domestic/refunds; $body [ out_trade_no $outTradeNo, out_refund_no $outRefundNo, reason $reason, notify_url https://api.example.com/refund_callback.php, amount [ refund $refundAmount, total $totalAmount, currency CNY ] ]; $result $this-request(POST, $path, json_encode($body, JSON_UNESCAPED_UNICODE), true); return $result; }注意amount.refund是本次退多少amount.total是订单原始总金额不是退款后的剩余金额。如果填反了微信会报参数错误。退款不一定立刻成功所以需要查询退款接口去同步状态。查询退款路径是GET /v3/refund/domestic/refunds/{out_refund_no}?mchid{mchid}返回信息里status字段代表退款状态PROCESSING退款处理中SUCCESS退款成功CLOSED退款关闭ABNORMAL退款异常可能需人工介入public function queryRefund(string $outRefundNo) { $path /v3/refund/domestic/refunds/ . $outRefundNo . ?mchid . $this-mchid; return $this-request(GET, $path); }如果退款账户余额不足微信会返回NOT_ENOUGH需要先充值这个错误不能靠重试解决要触发告警让财务去处理。另外部分退款场景下同一个原订单可以创建多个退款单每个退款单必须有独立的out_refund_no否则也会冲突。3.4 下载对账单并解析对账单接口返回的不是文件内容而是一个下载地址。先调用GET /v3/bill/tradebill?bill_date20240101bill_typeALL拿到download_url再请求这个地址才能拿到数据。退款账单对应/v3/bill/refundbill。public function downloadBill(string $date, string $billType ALL) { $path /v3/bill/tradebill?bill_date . $date . bill_type . $billType; $result $this-request(GET, $path); $content $this-httpGet($result[download_url]); // $content 是文本第一行是汇总第二行是表头后面是明细 return explode(\n, $content); }对账单文本用\t分隔第一行一般是统计信息表头在第二行从第三行开始才是每一笔订单。解析时要注意微信返回结果可能被gzip压缩download_url拿到后最好用支持gzip解压的HTTP客户端请求否则会看到乱码。账单解析逻辑虽然不复杂但我建议存到数据库之前先按“总笔数、总金额”做一次校验和账单头部汇总对不上时直接告警避免财务月结时发现问题。4. 实测中躲不开的坑和排查方法接口代码写完只是第一步真正联调的时候你会遇到一堆和代码无关的问题。我把这次实测里踩过的坑统一记下来希望对你有用。4.1 本地没法拉起支付UA与支付目录JSAPI支付只能在微信内置浏览器里调起。第一次调试时我在Chrome里打开页面调用WeixinJSBridge直接报undefined。后来看到不少人用PHP代码修改请求头里的User-Agent来模拟微信浏览器让服务端认为当前是微信环境。这个办法可以用来测页面加载和部分JS逻辑但它只能骗过服务端环境判断真正拉起支付的时候微信JSSDK还是需要微信内置浏览器环境。要真机调试最简单的办法是准备一个测试公众号在商户平台“产品中心-开发配置”里把JSAPI支付授权目录填上比如https://api.example.com/然后用微信扫一个内网映射出来或已备案的域名地址。如果目录配置不对调起时会报requestpayment:fail jsapi has no permission这是最常见的错误。另外公众号的JS接口安全域名也要配置一下和支付授权目录是两个概念。前者管JS-SDK签名后者管支付目录两个都别漏。一个页面要调起支付两个配置缺一不可其中任何一个没填都会让你在“看起来完全没问题”的代码里折腾半天。4.2 签名、证书、金额单位引发的连环错误排查签名问题时我习惯先打印请求的Authorization和微信返回的响应体再用官方文档里的签名示例逐行核对。这里有几个最容易踩的细节金额单位是分1元要传100。退款参数里total是订单原金额refund是退款金额不会自动计算。证书序列号指的是serial_no不是证书文件的文件名。有人把apiclient_key.pem里的私钥文本当成证书序列号去填请求永远返回401。商户私钥必须和证书序列号出自同一个商户号。在商户平台下载过的证书可以多套环境并存别把测试环境的证书配到生产环境的私钥上。生成调起参数时timeStamp必须是字符串。PHP里直接time()返回的是int某些端会校验类型导致签名失败强制(string)转一下最保险。回调通知里的数据使用AES-256-GCM加密解密时api_v3_key长度必须是32位缺少一位都会导致解密失败。4.3 小程序违规导致支付功能不可用的排查如果你做的是小程序里调起JSAPI支付还会遇到一种更头疼的情况页面一切正常但一点“支付”按钮就提示“小程序违规支付功能暂不可用”。这通常不是代码问题而是小程序账号被平台限制了支付能力。处理方式没有捷径只能登录小程序后台查看违规详情按要求整改或申诉等平台解限后再继续。在Demo里需要给这种情况加异常提示不要只吞掉错误否则用户会以为支付服务挂了。我的做法是把微信返回的code和message记录下来页面提示“支付通道异常请联系客服”同时给自己接口告警。写这个Demo时我踩得最多的不是接口逻辑而是各种配置和证书问题。建议先跑通最简单的下单再一步步加上查单、关单、退款和对账单千万别一上来就六个接口一起调。付款、退款、对账这套流程跑通了后面的问题基本都能按错误码一个个解决。本文还有配套的精品资源点击获取
返回列表