微信H5支付PHP完整接入教程:统一下单、回调处理与避坑指南
2026/9/9 15:08:50 网站建设 项目流程

简介:一套专为移动端网页支付场景设计的 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.chooseWXPay
H5手机非微信浏览器不需要MWEB跳转mweb_url
NativePC端扫码不需要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密钥。这里要特别说一句,微信支付团队现在主推APIv3,V2密钥的设置入口在新商户号里可能找不到。如果你是新注册的商户号,建议直接改走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_typeMWEB。这一步要做四件事:组装请求参数、生成签名、发送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 '<xml><return_code><![CDATA[FAIL]]></return_code><return_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 '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名错误]]></return_msg></xml>'; exit; } // 2. 查本地订单,校验金额是否一致 $order = getOrderByNo($data['out_trade_no']); // 你自己的查询函数 if (!$order || intval($order['amount'] * 100) !== intval($data['total_fee'])) { echo '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在或金额不一致]]></return_msg></xml>'; exit; } // 3. 幂等处理:订单已支付直接返回SUCCESS,避免重复发货 if ($order['status'] == 1) { echo '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'; exit; } // 4. 更新本地订单状态,记录微信交易号 updateOrderPaid($order['id'], $data['transaction_id']); // 5. 返回成功,微信收到后停止重发 echo '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_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_ADDR(H5支付场景下,微信对这个字段的校验没有严格到必填真实用户IP,但传错会影响支付风控,极端情况下会拦截交易)。

如果你遇到下单不跳转、回调收不到、验签失败这类问题,我一般按这个顺序排查:先看统一下单返回的return_msgerr_code_des;再看商户平台是否产生了这笔交易;然后看服务器回调日志里微信有没有发通知;最后核对验签用的key是否和商户平台一致。90%的问题都出在这四步里,代码本身反而是最后才需要怀疑的对象。

这套代码我在生产环境跑了挺长时间,期间也根据项目需求改过好几版,最后沉淀成文章里这个结构。实际用下来最大的体会是:支付功能九成的问题出在配置而不是代码,商户参数核对清楚,日志记录完整,跑通一次之后,后面接任何项目都会变得很顺手。另外再分享一个小优化:下单拿到mweb_url之后,可以在URL后面拼接&redirect_url=你的订单页地址并做URL编码,这样用户支付完成后会自动跳回你自己的页面,体验会比停在微信的支付完成页好很多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询