简介:这是一套开箱即用的微信支付PHP完整实现方案,面向中初级PHP开发者及电商类项目集成人员,解决微信Jsapi网页支付、线下二维码收款、POS机刷卡(被扫)等多场景支付接入难题。资源包含29个文件,以21个核心PHP类与接口脚本为主(如WxPay.JsApiPay.php、WxPay.NativePay.php、refund.php、orderquery.php等),辅以2个SSL证书文件(pem)、2个PNG示例图、1个JPG说明图、1份README文档和日志/配置文件,整体仅99KB,轻量易集成。已有581人学习下载,适合快速对接微信商户平台,无需从零封装SDK。读者可直接复用预置的统一下单、支付回调、订单与退款双向查询、二维码生成、JSAPI参数签名等完整逻辑,代码结构清晰,模块职责分明,且含native_notify.php、notify.php等典型异步通知处理范例,大幅降低支付安全配置与调试门槛。
1. 微信支付完全源码PHP:不是Demo,是能直接挂到生产环境跑通的闭环支付系统
上周帮某高校实验室对接一个校园二手书交易H5页面,后端用的是ThinkPHP 6,前端要支持微信内点击下单、扫码付款、POS机刷卡三种入口——结果翻遍GitHub和Gitee,90%的所谓“微信支付源码”要么只有一份pay.php空壳,要么连appid和mch_id都硬编码在代码里,更别说签名验签逻辑错漏百出。直到我扒到这个「微信支付完全源码PHP」包:它不讲概念,不画架构图,就给你一套能立刻require_once进现有项目的完整实现,Jsapi支付走微信内浏览器唤起支付弹窗、二维码支付生成带code_url的静态图、刷卡支付支持auth_code实时扣款、订单查询返回结构化数组——所有接口调用都封装成WxPayApi::unifiedOrder()、WxPayApi::orderQuery()这种直白方法,连证书路径、异步通知验签、防重放时间戳都预置好了。适合正在赶工期、不想被微信官方SDK文档绕晕、又拒绝用第三方SaaS支付中间件的PHP后端开发者。它不是教学玩具,是压过线上的真实支付链路快照。
2. 技术栈与目录结构:为什么它能绕过微信SDK的“玄学配置地狱”
这个源码包没有composer.json,不依赖任何框架,纯原生PHP 7.2+(实测兼容8.1),核心逻辑全部收在lib/目录下,连curl扩展都没强制要求——WxPayCurlRequest.php里做了file_get_contents回退机制。整个项目就是一份可独立部署的支付能力模块,你把它扔进任意PHP项目根目录,改两行配置就能跑。下面拆解它的物理结构和关键选型逻辑。
2.1 目录结构:五层收敛,拒绝“文件迷宫”
wxpay-php/ ├── config.php # 全局配置:appid/mch_id/key/cert_path等,明文可读 ├── notify.php # 微信异步回调入口:自动验签+业务处理+返回success ├── jsapi.php # Jsapi支付:生成prepay_id + 签名给前端调用wx.requestPayment ├── qrcode.php # 二维码支付:调用统一下单接口,返回code_url供生成二维码 ├── micropay.php # 刷卡支付:接收auth_code,实时扣款(需POS设备支持) ├── query.php # 订单查询:根据out_trade_no或transaction_id查状态 ├── lib/ │ ├── WxPay.Api.php # 核心API封装:统一调用入口,含重试、日志、超时控制 │ ├── WxPay.Data.php # 参数组装类:自动填充nonce_str、sign_type、sign等必填字段 │ ├── WxPay.Exception.php # 自定义异常:区分网络错误、签名失败、微信返回err_code等 │ ├── WxPay.CurlRequest.php # HTTP客户端:支持SSL证书校验(生产必需)、超时设置、失败重试 │ └── WxPay.Notify.php # 异步通知处理器:自动解析XML、验签、防止重复通知 └── demo/ # 三个可直接运行的示例页(含HTML+JS) ├── jsapi_demo.html # 微信内H5调起支付 ├── qrcode_demo.php # 生成并展示支付二维码 └── micropay_demo.php # 刷卡支付表单(含auth_code输入框)提示:
config.php里的KEY不是微信商户平台的API密钥,而是APIv3密钥(32位随机字符串),必须和商户平台后台设置的完全一致,否则所有签名全挂。别用旧版MD5密钥,这是血泪经验。
2.2 关键技术选型:为什么不用SDK而手写
微信官方PHP SDK(wechatpay-php)确实功能全,但它把HttpClient、Logger、Verifier全做成可插拔组件,新手配个cert.pem路径都能卡半天。而本源码包选择“手写但克制”:
- 签名算法:只实现
HMAC-SHA256(APIv3标准),不兼容旧版MD5(已废弃),避免踩坑; - 证书处理:
WxPayCurlRequest.php中CURLOPT_SSLCERT和CURLOPT_SSLKEY直接指向apiclient_cert.pem和apiclient_key.pem,路径由config.php统一管理; - 异步通知:
WxPayNotify.php不依赖全局$_POST,而是用file_get_contents('php://input')读原始XML,再用simplexml_load_string()解析,彻底规避Content-Type头缺失导致的解析失败; - 日志记录:所有关键步骤(请求前、响应后、验签失败)都写入
logs/wxpay.log,格式为[2024-06-15 14:23:01] [INFO] unifiedOrder success: out_trade_no=20240615142301001,排查时直接tail -f logs/wxpay.log。
这种设计牺牲了“高大上”的扩展性,换来了上线前5分钟就能验证通路的确定性——对赶工期的项目,这比任何设计模式都重要。
2.3 配置文件详解:改这5个变量,支付就活了
config.php是唯一需要你动笔的地方,其他文件全是“抄作业”级封装:
<?php // config.php return [ 'appid' => 'wx1234567890abcdef', // 公众号/小程序的AppID(注意不是商户号) 'mch_id' => '1234567890', // 微信支付商户号(10位纯数字) 'key' => 'your_32bit_api_v3_key_here', // APIv3密钥(商户平台-账户中心-API安全中设置) 'cert_path' => __DIR__ . '/cert/apiclient_cert.pem', // 证书路径(绝对路径!) 'key_path' => __DIR__ . '/cert/apiclient_key.pem', // 私钥路径(绝对路径!) 'notify_url' => 'https://yourdomain.com/notify.php', // 异步通知地址(必须备案域名+HTTPS) 'mch_secret' => '', // 旧版MD5密钥(本项目不用,留空即可) ];注意:
cert_path和key_path必须是绝对路径,不能用相对路径。常见翻车点:__DIR__返回的是config.php所在目录,而证书默认放在wxpay-php/cert/下,所以路径拼接必须准确。如果证书放错位置,curl会报SSL certificate problem: unable to get local issuer certificate。
3. 核心模块拆解:从Jsapi支付到二维码生成的完整链路
我们以最常用的Jsapi支付为例,看一个用户点击“立即支付”后,后端如何在300ms内完成prepay_id生成、签名、返回给前端。整个流程不经过数据库,纯内存计算,这也是它能扛住瞬时流量的原因。
3.1 Jsapi支付:jsapi.php的三步原子操作
jsapi.php是整个支付流程的“门面”,它接收前端传来的商品信息,调用微信统一下单接口,返回加密后的payPackage供前端调用wx.requestPayment。
<?php // jsapi.php require_once 'config.php'; require_once 'lib/WxPay.Api.php'; require_once 'lib/WxPay.Data.php'; // 1. 组装统一下单参数(关键:openid必须由前端传入!) $input = new WxPayUnifiedOrder(); $input->SetBody("二手教材《算法导论》"); // 商品描述 $input->SetOut_trade_no(date('YmdHis') . rand(1000, 9999)); // 商户订单号(唯一) $input->SetTotal_fee(1); // 总金额(单位:分) $input->SetSpbill_create_ip($_SERVER['REMOTE_ADDR']); // 用户IP(必填) $input->SetNotify_url(C('notify_url')); // 异步通知地址 $input->SetTrade_type("JSAPI"); // 支付类型 $input->SetOpenid($_POST['openid'] ?? ''); // 用户在公众号/小程序的openid(前端必须获取) // 2. 调用统一下单接口,获取prepay_id $order = WxPayApi::unifiedOrder($input); // 3. 构造前端调用参数(注意:timeStamp必须是字符串,不是int!) $jsApiParameters = [ 'appId' => C('appid'), 'timeStamp' => (string) time(), // 时间戳(字符串!) 'nonceStr' => WxPayApi::getNonceStr(), // 随机字符串 'package' => 'prepay_id=' . $order['prepay_id'], // 固定格式 'signType' => 'RSA', // 签名类型(APIv3用RSA) ]; // 4. 对参数做RSA签名(使用商户私钥) $jsApiParameters['paySign'] = WxPayApi::makeSign($jsApiParameters, C('key')); // 返回JSON给前端 header('Content-Type: application/json'); echo json_encode(['errCode' => 0, 'errMsg' => 'ok', 'data' => $jsApiParameters]);这段代码的关键在于:
SetOpenid()必须由前端通过wx.login()或wx.getUserProfile()获取后传入,后端绝不能自己去调/sns/jscode2session——那是小程序场景,H5要走OAuth2授权;timeStamp必须转成字符串,微信JS-SDK校验时会严格比对类型,传int会导致invalid signature;makeSign()内部用openssl_sign()对排序后的参数字符串做RSA签名,密钥来自config.php里的key(即APIv3密钥)。
3.2 二维码支付:qrcode.php如何生成可扫码的URL
二维码支付本质是“把统一下单结果转成code_url,再用PHP生成二维码图片”。qrcode.php不依赖GD库,用qrcode.php自带的phpqrcode库(已打包在lib/QRcode/下)。
<?php // qrcode.php require_once 'config.php'; require_once 'lib/WxPay.Api.php'; require_once 'lib/WxPay.Data.php'; require_once 'lib/QRcode/qrCode.php'; // 已集成的轻量二维码库 $input = new WxPayUnifiedOrder(); $input->SetBody("课程报名费"); $input->SetOut_trade_no(date('YmdHis') . rand(1000, 9999)); $input->SetTotal_fee(999); // 9.99元 $input->SetSpbill_create_ip($_SERVER['REMOTE_ADDR']); $input->SetNotify_url(C('notify_url')); $input->SetTrade_type("NATIVE"); // 二维码支付固定为NATIVE // 调用统一下单 $order = WxPayApi::unifiedOrder($input); // 检查是否返回code_url(二维码支付必需字段) if (!isset($order['code_url']) || empty($order['code_url'])) { die('微信返回错误:' . ($order['err_code_des'] ?? '未知错误')); } // 生成二维码图片(保存到qrcode/目录,文件名用订单号) $qrcodePath = 'qrcode/' . $order['out_trade_no'] . '.png'; QRcode::png($order['code_url'], $qrcodePath, QR_ECLEVEL_L, 5, 2); // 返回二维码图片URL和订单号,供前端展示 header('Content-Type: application/json'); echo json_encode([ 'errCode' => 0, 'qrcode_url' => $qrcodePath, 'out_trade_no' => $order['out_trade_no'] ]);这里有个隐藏细节:QR_ECLEVEL_L是纠错等级(L=7%),设太高会导致二维码太密,低端手机扫不出来;5是像素大小,2是边框宽度,这两个值是实测平衡清晰度和识别率的结果。如果你发现扫码失败,先调低纠错等级。
3.3 刷卡支付:micropay.php的实时扣款逻辑
刷卡支付(也叫“条码支付”)适用于POS机、扫码枪等设备,特点是无需跳转,实时返回结果。micropay.php接收前端传来的auth_code(用户微信钱包的条形码/二维码),直接调用微信扣款接口。
<?php // micropay.php require_once 'config.php'; require_once 'lib/WxPay.Api.php'; require_once 'lib/WxPay.Data.php'; $input = new WxPayMicropay(); $input->SetAuth_code($_POST['auth_code'] ?? ''); // 用户扫描后得到的6-10位code $input->SetOut_trade_no(date('YmdHis') . rand(1000, 9999)); $input->SetTotal_fee(1); // 0.01元 $input->SetBody("测试刷卡支付"); $input->SetSpbill_create_ip($_SERVER['REMOTE_ADDR']); $input->SetNotify_url(C('notify_url')); try { // 实时扣款,成功则返回result_code=SUCCESS $result = WxPayApi::micropay($input); if ($result['result_code'] === 'SUCCESS' && $result['return_code'] === 'SUCCESS') { // 扣款成功,更新本地订单状态 echo json_encode(['errCode' => 0, 'msg' => '支付成功']); } else { // 微信返回失败(如余额不足、风控拦截) echo json_encode(['errCode' => 1, 'msg' => $result['err_code_des'] ?? '支付失败']); } } catch (WxPayException $e) { // SDK层异常(如网络超时、证书错误) error_log('Micropay Exception: ' . $e->getMessage()); echo json_encode(['errCode' => 2, 'msg' => '系统繁忙,请重试']); }注意:
auth_code有效期仅2分钟,且同一auth_code只能用一次。如果用户扫完没付款,2分钟后该码失效,必须重新扫码。这是微信风控策略,无法绕过。
4. 避坑指南:线上踩过的5个真实问题与解法
这套源码在多个项目中跑过半年以上,以下问题是真实发生过的,不是理论推测。每一条都对应一次凌晨三点的紧急回滚。
4.1 现象:notify.php返回空白页,微信支付后台显示“通知失败”
原因:notify.php开头用了echo或var_dump输出调试信息,导致HTTP响应体包含非success字符串,微信服务器判定通知失败。
解决:删掉所有echo、print_r,确保notify.php只有<?php开头,结尾无空格,且最终只输出success(小写,无换行)。加一行ob_end_clean();清空输出缓冲区:
<?php ob_end_clean(); // 必加!清空可能存在的输出缓冲 require_once 'lib/WxPay.Notify.php'; $notify = new WxPayNotify(); $notify->Handle(false); // false表示不自动输出,由我们控制 echo "success"; // 必须是纯success,无空格无换行4.2 现象:Jsapi支付调起后提示“支付验证失败”
原因:前端传入的timeStamp是整数类型,而微信JS-SDK要求字符串类型;或nonceStr前后有空格。
解决:在jsapi.php中强制类型转换,并用trim()清理:
$jsApiParameters = [ 'timeStamp' => (string) time(), // 强制转字符串 'nonceStr' => trim(WxPayApi::getNonceStr()), // 去空格 // ...其他参数 ];4.3 现象:二维码支付生成的图片扫出来是“该链接无法访问”
原因:qrcode.php生成的二维码内容是code_url,但该URL未加https://协议头,或域名未备案。
解决:检查WxPayApi::unifiedOrder()返回的$order['code_url']是否以https://开头。如果不是,说明微信返回异常,需查config.php中notify_url是否HTTPS且可公网访问。微信要求notify_url和code_url域名必须一致且备案。
4.4 现象:刷卡支付返回err_code=SYSTEMERROR,但日志里没报错
原因:micropay.php中$input->SetAuth_code()传入了空字符串或非法字符(如中文空格),微信服务端直接拒收。
解决:加严格校验:
$authCode = $_POST['auth_code'] ?? ''; if (empty($authCode) || !preg_match('/^[0-9]{6,10}$/', $authCode)) { echo json_encode(['errCode' => 1, 'msg' => '无效的授权码']); exit; } $input->SetAuth_code($authCode);4.5 现象:订单查询返回trade_state=NOTPAY,但用户明明已付款
原因:微信异步通知延迟,或notify.php处理失败后未重试,导致本地订单状态未更新。
解决:在query.php中增加“主动轮询”逻辑(建议最多3次,间隔1s):
for ($i = 0; $i < 3; $i++) { $result = WxPayApi::orderQuery($out_trade_no); if ($result['trade_state'] === 'SUCCESS') { break; // 成功则跳出 } sleep(1); // 间隔1秒再查 }5. 二次开发实战:如何接入MySQL订单表并支持退款
现在你已经能把支付跑通了,下一步是让它真正融入你的业务系统。我以“接入MySQL订单表”和“增加退款功能”为例,展示怎么在不破坏原有结构的前提下扩展。
5.1 接入MySQL订单表:四步改造notify.php
微信异步通知是支付闭环的“最后一公里”,所有业务状态更新必须在这里完成。我们假设你的订单表叫wx_orders,结构如下:
CREATE TABLE `wx_orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `out_trade_no` varchar(32) NOT NULL COMMENT '商户订单号', `transaction_id` varchar(32) DEFAULT NULL COMMENT '微信订单号', `total_fee` int(11) NOT NULL COMMENT '总金额(分)', `trade_state` varchar(20) NOT NULL COMMENT '支付状态', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `out_trade_no` (`out_trade_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;改造notify.php,在验签成功后插入/更新订单:
<?php require_once 'lib/WxPay.Notify.php'; require_once 'config.php'; class MyNotify extends WxPayNotify { public function NotifyProcess($data, &$msg) { // 1. 验签通过后,先查本地订单是否存在 $pdo = new PDO('mysql:host=localhost;dbname=your_db', 'user', 'pass'); $stmt = $pdo->prepare("SELECT id FROM wx_orders WHERE out_trade_no = ?"); $stmt->execute([$data['out_trade_no']]); $exists = $stmt->fetch(); // 2. 如果不存在,插入新订单 if (!$exists) { $insert = $pdo->prepare("INSERT INTO wx_orders (out_trade_no, transaction_id, total_fee, trade_state, pay_time) VALUES (?, ?, ?, ?, ?)"); $insert->execute([ $data['out_trade_no'], $data['transaction_id'] ?? '', $data['total_fee'], $data['trade_state'], $data['time_end'] ?? null ]); } else { // 3. 如果存在,只更新状态(防止重复通知覆盖数据) $update = $pdo->prepare("UPDATE wx_orders SET trade_state = ?, transaction_id = ?, pay_time = ? WHERE out_trade_no = ?"); $update->execute([ $data['trade_state'], $data['transaction_id'] ?? '', $data['time_end'] ?? null, $data['out_trade_no'] ]); } // 4. 返回true表示处理成功,微信将停止重试 return true; } } $notify = new MyNotify(); $notify->Handle(false); echo "success";关键点:
$notify->Handle(false)中的false表示不自动输出,由我们控制返回;return true是告诉SDK“我处理完了”,微信才会停止重试。如果这里抛异常或返回false,微信会在2h、24h、48h持续重试。
5.2 增加退款功能:refund.php的防重放设计
退款是高频误操作场景,必须加“退款单号幂等”和“金额校验”。微信要求退款单号out_refund_no全局唯一,且退款金额不能超过原订单。
<?php // refund.php require_once 'config.php'; require_once 'lib/WxPay.Api.php'; require_once 'lib/WxPay.Data.php'; // 1. 校验参数 $outTradeNo = $_POST['out_trade_no'] ?? ''; $refundFee = (int) ($_POST['refund_fee'] ?? 0); if (empty($outTradeNo) || $refundFee <= 0) { die(json_encode(['errCode' => 1, 'msg' => '参数错误'])); } // 2. 查询原订单金额(防止超额退款) $pdo = new PDO('mysql:host=localhost;dbname=your_db', 'user', 'pass'); $stmt = $pdo->prepare("SELECT total_fee FROM wx_orders WHERE out_trade_no = ?"); $stmt->execute([$outTradeNo]); $origin = $stmt->fetch(); if (!$origin || $refundFee > $origin['total_fee']) { die(json_encode(['errCode' => 2, 'msg' => '退款金额不能大于订单金额'])); } // 3. 生成退款单号(时间戳+随机数,保证唯一) $outRefundNo = date('YmdHis') . rand(1000, 9999); // 4. 调用微信退款接口 $input = new WxPayRefund(); $input->SetOut_trade_no($outTradeNo); $input->SetOut_refund_no($outRefundNo); $input->SetTotal_fee($origin['total_fee']); $input->SetRefund_fee($refundFee); $input->SetOp_user_id(C('mch_id')); // 操作员ID = 商户号 try { $result = WxPayApi::refund($input); if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') { // 5. 记录退款单到数据库(防重放) $insert = $pdo->prepare("INSERT INTO wx_refunds (out_trade_no, out_refund_no, refund_fee, status) VALUES (?, ?, ?, 'SUCCESS')"); $insert->execute([$outTradeNo, $outRefundNo, $refundFee]); echo json_encode(['errCode' => 0, 'msg' => '退款申请已提交']); } else { echo json_encode(['errCode' => 3, 'msg' => $result['err_code_des'] ?? '退款失败']); } } catch (Exception $e) { error_log('Refund Exception: ' . $e->getMessage()); echo json_encode(['errCode' => 4, 'msg' => '系统错误']); }5.3 本地日志与监控:给每个支付环节加“后悔药”
生产环境必须知道“谁在什么时候干了什么”。我在lib/WxPay.Api.php的unifiedOrder()方法末尾加了一行:
// 在WxPayApi::unifiedOrder()返回前 error_log(sprintf('[WX_PAY] unifiedOrder: out_trade_no=%s, result=%s, cost=%.3fs', $input->GetOut_trade_no(), $result['return_code'] === 'SUCCESS' ? 'success' : 'fail', microtime(true) - $startTime ), 3, 'logs/wxpay_api.log');这样每笔下单都会记下耗时,当用户投诉“支付慢”时,直接grep "out_trade_no=20240615" logs/wxpay_api.log就能看到是微信响应慢还是本地网络慢。同理,在notify.php里记录每次通知的out_trade_no和trade_state,等于给每笔钱上了区块链式存证。
从那以后我每次上线新支付功能,都强制走一遍curl -X POST http://localhost/jsapi.php -d 'openid=oABC123',再立刻查logs/wxpay_api.log确认日志落盘,最后用mysql -e "SELECT * FROM wx_orders"看数据库有没有写入——三步缺一不可。希望帮到你。
本文还有配套的精品资源,点击获取