微信支付PHP生产级源码:Jsapi/扫码/刷卡全链路实现
简介这是一套开箱即用的微信支付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和Gitee90%的所谓“微信支付源码”要么只有一份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/ # 三个可直接运行的示例页含HTMLJS ├── jsapi_demo.html # 微信内H5调起支付 ├── qrcode_demo.php # 生成并展示支付二维码 └── micropay_demo.php # 刷卡支付表单含auth_code输入框提示config.php里的KEY不是微信商户平台的API密钥而是APIv3密钥32位随机字符串必须和商户平台后台设置的完全一致否则所有签名全挂。别用旧版MD5密钥这是血泪经验。2.2 关键技术选型为什么不用SDK而手写微信官方PHP SDKwechatpay-php确实功能全但它把HttpClient、Logger、Verifier全做成可插拔组件新手配个cert.pem路径都能卡半天。而本源码包选择“手写但克制”签名算法只实现HMAC-SHA256APIv3标准不兼容旧版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_no20240615142301001排查时直接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 signaturemakeSign()内部用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是纠错等级L7%设太高会导致二维码太密低端手机扫不出来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_codeSUCCESS $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_codeSYSTEMERROR但日志里没报错原因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_stateNOTPAY但用户明明已付款原因微信异步通知延迟或notify.php处理失败后未重试导致本地订单状态未更新。解决在query.php中增加“主动轮询”逻辑建议最多3次间隔1sfor ($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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;改造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:hostlocalhost;dbnameyour_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:hostlocalhost;dbnameyour_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_no20240615 logs/wxpay_api.log就能看到是微信响应慢还是本地网络慢。同理在notify.php里记录每次通知的out_trade_no和trade_state等于给每笔钱上了区块链式存证。从那以后我每次上线新支付功能都强制走一遍curl -X POST http://localhost/jsapi.php -d openidoABC123再立刻查logs/wxpay_api.log确认日志落盘最后用mysql -e SELECT * FROM wx_orders看数据库有没有写入——三步缺一不可。希望帮到你。本文还有配套的精品资源点击获取