ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微信H5支付PHP完整接入教程:统一下单、回调处理与避坑指南

微信H5支付PHP完整接入教程:统一下单、回调处理与避坑指南 简介一套专为移动端网页支付场景设计的 PHP 微信 H5 支付完整代码面向需要快速接入微信 H5 支付的 PHP 开发者。压缩包内共 3 个文件包含 2 个 PHP 文件主支付逻辑与异步回调处理及 1 个 txt 使用说明整体仅 5KB结构精简便于直接部署改造。代码覆盖统一下单、预支付订单生成、支付结果回调验签、订单状态处理等核心流程回调结果会写入 log 文件方便调试与日志分析。使用时只需替换商户号、API 密钥等商户资料并配置好 notify_url 回调地址即可运行。已有 1811 人学习下载适合需要快速理解微信 H5 支付交互逻辑、并在自有项目中实现移动端收款的开发者参考。 做网站服务的这几年被问得最多的问题之一就是微信H5支付到底怎么接。网上的教程一搜一大把但真能一次跑通的太少——要么只讲下单流程回调处理一笔带过要么代码跑起来全是坑签名错误、支付授权目录不对、微信一直重发回调通知。我自己第一次接H5支付也翻了大半天的文档最后才沉淀出这套完整的PHP微信H5支付代码。这篇文章就把这套代码完整分享出来包含下单、唤醒支付、后台回调处理三部分你只需要把自己的AppID、商户号、API密钥和回调地址替换进去就能直接用。要提醒的是微信H5支付指的是在手机浏览器非微信内置浏览器里拉起微信App完成的支付和公众号里的JSAPI支付、扫码的Native支付是完全不同的三条技术路线千万别选错。1. 先搞清楚H5支付到底解决什么问题1.1 H5支付和JSAPI、Native的区别很多新手第一次接支付最容易懵的地方就在这里同样是微信支付为什么有时候要传openid有时候不用有时候返回一个跳转链接有时候返回一张二维码。H5支付的典型使用场景是用户在用浏览器打开H5商城、活动落地页或者从广告页跳转到商品详情页点击微信支付按钮后系统唤起微信客户端完成付款。这种模式下不需要用户授权登录微信因为微信App本身就是通过H5页面携带的mweb_url拉起支付的所以不需要openid。而JSAPI支付只能在微信内置浏览器里使用流程是先通过网页授权拿到用户的openid再用JSAPI下单获取支付参数最后调起wx.chooseWXPay完成支付。这个模式的限制非常明显离开微信浏览器就玩不转。Native支付则是PC端扫码场景下单后会返回一个code_url你生成二维码给用户扫和H5支付的交互逻辑完全不同。三种方式的关键差异可以看这张表支付方式适用场景是否需要openidtrade_type拉起方式JSAPI微信内置浏览器、公众号菜单需要JSAPIwx.chooseWXPayH5手机非微信浏览器不需要MWEB跳转mweb_urlNativePC端扫码不需要NATIVE生成二维码链接1.2 H5支付的适用条件明确了定位之后还要确认你的商户号是否满足H5支付的开通条件。H5支付需要在微信商户平台后台单独开通虽然是自动审核类的产品但有几个硬性前提第一商户号必须已经完成了微信支付认证且开通了H5支付产品。在商户平台的产品中心里能看到H5支付的入口如果没开通就先去申请一般即时生效。第二需要在商户平台配置H5支付域名。这个域名会被微信用来校验H5页面是否合法配置规则是一个根域名下最多可以增加四个子域名支付页面的URL必须落在配置的域名范围内。第三回调地址必须是HTTPS协议的公网地址。微信统一支付接口对notify_url有明确要求如果是http下单时会直接报错。第四也是很多人忽略的一点H5支付不能在微信内置浏览器里正常拉起支付微信会拦截跨客户端跳转并提示不允许跨客户端支付。所以实际项目中一般先判断UserAgent如果包含MicroMessenger就提示用户改用右上角菜单在浏览器打开或者直接切换到JSAPI支付逻辑。2. 动手前需要准备的四样东西2.1 四个配置项从哪里找这套代码宣称改好商户资料就能用那到底要改哪几个地方我把整个流程跑完发现真正必须改的就四个配置项AppID、商户号、API密钥、回调地址。配置项获取位置说明AppID微信公众平台/开放平台H5支付一般用服务号或开放平台绑定的应用AppID商户号mch_id微信商户平台首页商户平台的唯一标识注意和AppID的绑定关系API密钥key商户平台-账户中心-API安全32位字符串V2接口签名用的就是它回调地址notify_url自己的服务器HTTPS绝对地址指向notify.php其中最容易搞混的是AppID。如果你是服务号里的商户号AppID就用服务号的AppID如果你是开放平台的应用AppID就是开放平台应用对应的AppID。总之这个AppID必须已经和你的商户号完成了关联绑定否则统一下单时会返回appid和mch_id不匹配。API密钥的设置路径是商户平台-账户中心-API安全-APIv2密钥。这里要特别说一句微信支付团队现在主推APIv3V2密钥的设置入口在新商户号里可能找不到。如果你是新注册的商户号建议直接改走APIv3接口用商户私钥平台证书做RSA签名业务逻辑和这套代码完全一致只是签名和验签的方式不同。如果你的商户号是存量老号V2接口仍然可以正常使用。2.2 回调地址的要求回调地址是整套代码里最容易被轻敌的一个环节。很多项目本地调试正常一上线发现支付成功但订单状态不更新问题十有八九出在回调地址上。微信支付回调地址有四个硬性要求必须HTTPS必须公网可达不能带路径参数或者带了也要保证服务端能准确识别页面跳转不能影响回调接口的响应。我见过有人把notify_url写成了订单确认页的URL结果微信每次回调都跳到一个HTML页面服务端解析不到XML数据订单就永远卡在待支付状态。另外回调接口返回给微信的数据必须是微信能识别的XML格式而且要在业务处理完成之后立即返回不要在回调里去做发送短信、生成物流单这类耗时操作。微信等待回调响应的超时时间很短你要是处理太慢微信就会判定为接收失败然后按既定频率重发通知。3. 完整代码下单、跳转、回调三段式结构3.1 项目文件结构这套代码一共四个文件结构很清晰wechat_h5_pay/ ├── config.php // 商户配置唯一需要改的文件 ├── WxpayH5.php // 核心类签名、请求、解析XML ├── pay.php // 下单入口生成订单并跳转支付 └── notify.php // 回调处理验签、更新订单、响应微信config.php里只需要维护四个常量?php // config.php - 微信H5支付配置 define(WX_APPID, 你的AppID); define(WX_MCH_ID, 你的商户号); define(WX_API_KEY, 你的API密钥); define(WX_NOTIFY_URL, https://你的域名/notify.php);3.2 下单接口实现下单的核心是调用微信统一下单接口H5支付的trade_type是MWEB。这一步要做四件事组装请求参数、生成签名、发送POST请求、解析返回结果。// WxpayH5.php class WxpayH5 { private $appid; private $mchId; private $apiKey; public function __construct($appid, $mchId, $apiKey) { $this-appid $appid; $this-mchId $mchId; $this-apiKey $apiKey; } // 生成签名除sign外非空字段按ASCII排序拼接key后MD5 public function sign($params) { ksort($params); $str ; foreach ($params as $k $v) { if ($v ! !is_array($v)) { $str . $k . . $v . ; } } $str . key . $this-apiKey; return strtoupper(md5($str)); } // 数组转XML统一用CDATA避免中文和特殊字符问题 public function toXml($params) { $xml xml; foreach ($params as $k $v) { if ($v ! !is_array($v)) { $xml . . $k . ![CDATA[ . $v . ]]/ . $k . ; } } $xml . /xml; return $xml; } // 统一下单 public function unifiedOrder($order) { $params [ appid $this-appid, mch_id $this-mchId, nonce_str md5(uniqid(wx_, true)), body $order[body], out_trade_no $order[out_trade_no], total_fee intval($order[total_fee]), spbill_create_ip $order[client_ip], notify_url $order[notify_url], trade_type MWEB, scene_info json_encode([ h5_info [ type Wap, wap_url $order[wap_url], wap_name $order[wap_name] ] ], JSON_UNESCAPED_UNICODE) ]; $params[sign] $this-sign($params); $xml $this-toXml($params); $response $this-postXml(https://api.mch.weixin.qq.com/pay/unifiedorder, $xml); return $this-parseXml($response); } }下单入口pay.php组装订单并发起跳转?php require config.php; require WxpayH5.php; // 生成商户订单号业务项目里一般由订单表生成 $orderNo date(YmdHis) . mt_rand(1000, 9999); $amount 1; // 单位元 $wxpay new WxpayH5(WX_APPID, WX_MCH_ID, WX_API_KEY); $result $wxpay-unifiedOrder([ body 测试商品, out_trade_no $orderNo, total_fee $amount * 100, // 元转分 client_ip $_SERVER[REMOTE_ADDR], notify_url WX_NOTIFY_URL, wap_url https://你的域名/order.php, wap_name 我的商城 ]); if ($result[return_code] SUCCESS $result[result_code] SUCCESS) { // 拿到mweb_url后可以直接跳转也可以返回JSON给前端处理 header(Location: . $result[mweb_url]); exit; } // 失败时打印错误信息方便排查 var_dump($result);这里有个细节必须强调total_fee的单位是分。很多线下接口用元微信支付统一用的是分$amount * 100这一步千万不能漏。另外scene_info不能省略H5支付下单时如果场景信息不合法接口会直接报错后面避坑章节我会详细展开。3.3 回调处理逻辑回调是整个支付环节的后半场也是标题里特别强调的回调后台代码。?php require config.php; require WxpayH5.php; // 微信推送的是原始XML数据 $xml file_get_contents(php://input); // 记录原始回调线上排查问题就靠它 file_put_contents(notify_ . date(Ymd) . .log, date(Y-m-d H:i:s) . . $xml . PHP_EOL, FILE_APPEND); $wxpay new WxpayH5(WX_APPID, WX_MCH_ID, WX_API_KEY); $data $wxpay-parseXml($xml); if ($data[return_code] ! SUCCESS || $data[result_code] ! SUCCESS) { echo xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[支付失败]]/return_msg/xml; exit; } // 1. 验签拿到sign并且在原始参数里移除sign再重新签名 $sign $data[sign]; unset($data[sign]); if ($sign ! $wxpay-sign($data)) { file_put_contents(notify_ . date(Ymd) . .log, 验签失败 . PHP_EOL, FILE_APPEND); echo xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[签名错误]]/return_msg/xml; exit; } // 2. 查本地订单校验金额是否一致 $order getOrderByNo($data[out_trade_no]); // 你自己的查询函数 if (!$order || intval($order[amount] * 100) ! intval($data[total_fee])) { echo xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[订单不存在或金额不一致]]/return_msg/xml; exit; } // 3. 幂等处理订单已支付直接返回SUCCESS避免重复发货 if ($order[status] 1) { echo xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml; exit; } // 4. 更新本地订单状态记录微信交易号 updateOrderPaid($order[id], $data[transaction_id]); // 5. 返回成功微信收到后停止重发 echo xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml;第4步的updateOrderPaid就是你自己项目的订单更新逻辑这里用伪代码占位。回调里一定要先验签、再查单、再更新状态这个顺序不能乱。4. 回调里验签和订单状态检查为什么不能省很多人觉得回调处理能跑就行验签可有可无。这绝对是个危险念头。支付回调接口就是一个公网POST接口任何人拿到你的回调地址都可以构造一个假的XML通知过来。如果不验签攻击者直接伪造一笔已支付通知你的系统就会给一个没付钱的订单发货这损失不是开玩笑的。验签的原理和下单签名完全一致把微信回调过来的所有参数sign除外按ASCII字典序排序剔除空值末尾拼接key你的API密钥然后做MD5并转换成大写和回调XML里的sign字段比对。这里有一个特别容易踩的细节验签前一定要把sign字段从参数数组里移除否则签名永远对不上。验签通过之后紧接着要做两笔业务核对。第一笔是金额核对拿回调里的out_trade_no去查本地订单比对订单金额和total_fee是否一致。微信回调里的金额是用户在微信侧实际支付的金额理论上不会和本地订单有差异但为了防止被篡改、防止下单金额和实际金额不一致的情况这一步必须做。第二笔是订单状态核对如果订单已经是已支付状态说明这笔回调是重复通知直接返回SUCCESS即可不要再重复更新库存、重复发货。关于重复通知微信支付官方有一套重试机制如果商户没有在规定时间内返回SUCCESS微信会按递增间隔多次重发通知最长可以持续数天。所以回调接口必须做到幂等也就是处理一次和处理多次的结果完全一致。这段话的实操含义是你的回调代码里不能只写更新订单状态这个动作还要先判断订单当前状态只有未支付状态才执行更新否则直接返回成功。日志记录同样不能省。我在notify.php里写的那行file_put_contents看着简单实际排查线上问题的时候几乎就是救命稻草。微信的回调通知是主动POST过来的不像浏览器请求那样有迹可循一旦订单状态没更新第一步就是打开回调日志看微信到底有没有发通知、发过来的内容是什么、验签是否通过。我强烈建议日志里除了原始XML还要记录验签结果、订单查询结果、最终处理结果这样排查链路才完整。5. 上线时最容易踩的五个坑5.1 金额单位分和元必须分清这个坑我在第3章提过一次但因为真的害过不少人必须再展开说。微信支付所有的金额单位都是分不是元。无论是下单参数total_fee还是回调通知里的total_fee全部是分。常见错误有两种一种是后端直接把前端传过来的元金额当成total_fee传出去用户付1元结果微信账单显示1分另一种是回调比对时忘记把本地订单的元转成分子再比较导致金额永远对不上订单永远无法标记已支付。我的经验是项目里统一用分作为订单金额的存储单位或者在下单前统一做一次元转分从根源上避免混乱。5.2 支付域名和scene_info配置H5支付的scene_info是单独传的一个JSON字符串在V2接口里它长这样{ h5_info: { type: Wap, wap_url: https://你的域名/order.php, wap_name: 我的商城 } }很多第一次接的人会漏掉这个参数结果微信返回H5支付必须填写场景信息。即使填了还有第二个坑wap_url的域名必须和商户平台后台配置的H5支付域名一致。如果你在商户后台配置的是pay.example.com但下单场景里的wap_url写的是www.example.com统一下单接口照样报错。上线前先核对这两处能省不少时间。5.3 微信内置浏览器里的UA判断H5支付的代码本身在微信内置浏览器里也能下单甚至能拿到mweb_url但跳过去之后微信会拦截提示不允许跨客户端支付。这不是代码问题是平台规则。所以正规做法是在发起支付前判断浏览器的UserAgent如果包含MicroMessenger就引导用户通过右上角菜单选择在系统浏览器打开当前页面再继续支付。如果你们的业务同时支持微信公众号内支付也可以直接切换成JSAPI支付方案。5.4 curl请求微信接口的SSL证书问题服务器请求api.mch.weixin.qq.com时如果服务器上没有配置完整的根证书链curl会报SSL certificate problem导致统一下单失败。开发阶段很多人直接用CURLOPT_SSL_VERIFYPEER false绕过校验这个能跑通但生产环境不建议长期这么干更适合的做法是下载微信官方推荐的cacert.pem证书包然后在curl里设置CURLOPT_CAINFO指向该文件。另外别忘了给curl设置超时时间我一般设置30秒防止微信接口异常时请求一直挂着。5.5 用户IP字段不能随便传统一下单里的spbill_create_ip按官方文档要求是用户终端IP。如果你的服务器前面挂了Nginx反向代理或者CDN$_SERVER[REMOTE_ADDR]拿到的是代理服务器IP不是用户真实IP。这种情况下要么在Nginx里配置proxy_set_header X-Real-IP $remote_addr;然后在PHP里读取HTTP_X_REAL_IP要么直接沿用REMOTE_ADDRH5支付场景下微信对这个字段的校验没有严格到必填真实用户IP但传错会影响支付风控极端情况下会拦截交易。如果你遇到下单不跳转、回调收不到、验签失败这类问题我一般按这个顺序排查先看统一下单返回的return_msg和err_code_des再看商户平台是否产生了这笔交易然后看服务器回调日志里微信有没有发通知最后核对验签用的key是否和商户平台一致。90%的问题都出在这四步里代码本身反而是最后才需要怀疑的对象。这套代码我在生产环境跑了挺长时间期间也根据项目需求改过好几版最后沉淀成文章里这个结构。实际用下来最大的体会是支付功能九成的问题出在配置而不是代码商户参数核对清楚日志记录完整跑通一次之后后面接任何项目都会变得很顺手。另外再分享一个小优化下单拿到mweb_url之后可以在URL后面拼接redirect_url你的订单页地址并做URL编码这样用户支付完成后会自动跳回你自己的页面体验会比停在微信的支付完成页好很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进