
简介这份资源面向使用ThinkPHP5框架开发电商或在线业务的PHP开发者聚焦微信支付与支付宝支付的接口集成同时涉及网银支付对接思路。内容围绕商户信息配置、统一下单、预支付会话标识生成、支付回调处理及签名验证等关键环节展开帮助开发者理解支付接口的工作原理与安全传输要求。压缩包共2321个文件以1545个php源码文件为主体辅以data数据文件、phpt测试用例、md说明文档、xml与yml配置、json数据及少量图片与字体资源整体约4.76MB目录结构完整便于按模块查阅与二次开发。目前已有926人学习下载适合需要快速搭建支付功能、对照配置参数与排错思路的中级开发者参考借鉴。1. 从零跑通 TP5 双支付为什么我建议先啃下这套源码包上周有个做外包的朋友半夜找我说客户临时要在已经上线的 ThinkPHP5 商城项目里加微信支付和支付宝支付他翻了两天官方文档签名验签那一段始终对不上回调地址配了三遍还是 404。这种场景其实特别典型——TP5 项目做支付集成难的不是调 API而是把两套完全不同的签名体系、回调机制、订单状态流转塞进同一个 MVC 骨架里还不打架。这份资源包就是干这个的它把微信支付和支付宝支付在 ThinkPHP5 下的完整接入流程拆成了可运行的模块包含统一下单、签名生成、异步回调验签、订单状态回写这几个核心环节。适合正在用 TP5 做电商、知识付费或任何需要收款的开发者尤其是那种「文档看得懂但一上手就翻车」的阶段。下面我按实际拆包和跑通的顺序把关键节点和参数配置讲清楚。2. 环境准备与目录结构把两套 SDK 塞进 TP5 的正确姿势2.1 依赖版本与扩展检查TP5 对 PHP 版本有硬性要求微信支付 V3 接口和支付宝新版 SDK 又各自有依赖先把地基打牢。我一般会先跑一遍环境检查避免后面报错时在「是代码问题还是环境问题」之间反复横跳。# 检查 PHP 版本TP5.1 建议 7.1 以上V3 接口建议 7.2 php -v # 检查必要扩展curl 和 openssl 是支付签名的基础 php -m | grep -E curl|openssl|bcmath # 检查 composer 是否可用SDK 通过 composer 管理 composer --version逻辑说明php -v确认版本低于 7.1 的 TP5 项目在跑微信 V3 时会因为openssl签名函数参数差异直接抛异常。php -m过滤出 curl、openssl、bcmath 三个扩展bcmath 在金额计算时避免浮点误差支付宝 SDK 内部会用到。composer 版本建议 2.x1.x 在拉取某些依赖时会有兼容提示。参数说明如果你的项目还在 PHP 5.6微信支付 V2 接口勉强能跑但支付宝新版 SDK 已经不支持建议先升级 PHP 再动支付模块。扩展缺失时Linux 下用apt install php-curl php-openssl php-bcmath或编译安装Windows 下在 php.ini 里去掉对应扩展前面的分号。2.2 目录规划与 SDK 引入TP5 的目录结构比较自由但支付模块涉及配置文件、证书、日志乱放后期维护很痛苦。我习惯在application下单独建一个pay模块把两套逻辑隔离公共部分抽到common里。# 在 TP5 根目录执行创建支付模块目录 cd /path/to/tp5-project mkdir -p application/pay/controller mkdir -p application/pay/logic mkdir -p application/pay/config mkdir -p extend/pay/wechat mkdir -p extend/pay/alipay mkdir -p runtime/pay_log # 通过 composer 引入官方 SDK composer require wechatpay/wechatpay-guzzle-middleware composer require alipay/easysdk逻辑说明application/pay/controller放对外暴露的支付入口和回调控制器logic放订单处理、签名封装等业务逻辑config放商户号、密钥、证书路径等配置。extend/pay下分别放微信和支付宝的证书文件微信的apiclient_cert.pem和apiclient_key.pem必须放在这个目录且权限设为 600。runtime/pay_log专门存支付回调的原始报文出问题时这是唯一的「后悔药」。参数说明composer 包名根据实际 SDK 版本可能略有差异微信 V3 推荐用官方wechatpay-guzzle-middleware支付宝用alipay/easysdk。如果项目不能上 composer也可以手动下载 SDK 放到extend下但要注意命名空间和 TP5 的自动加载规则匹配手动引入时在extend目录下建对应文件夹TP5 会自动注册extend为根命名空间。注意微信支付证书文件不要提交到 Git 仓库在.gitignore里加上extend/pay/wechat/*.pem线上环境通过部署脚本单独上传。3. 微信支付接入统一下单、签名与回调验签的完整链路3.1 统一下单接口封装微信支付 V3 的统一下单接口是POST /v3/pay/transactions/jsapi和 V2 的 XML 报文完全不同V3 用 JSON 且签名走SHA256-RSA。很多从 V2 迁移过来的项目在这里翻车因为签名串的拼接规则变了。?php // application/pay/logic/WechatPay.php namespace app\pay\logic; use think\facade\Config; use think\facade\Log; class WechatPay { // 统一下单返回 prepay_id public function unifiedOrder($orderNo, $amount, $openid, $desc) { $config Config::get(pay.wechat); $url https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi; // 构造请求体金额单位是分 $body [ appid $config[appid], mchid $config[mchid], description $desc, out_trade_no $orderNo, notify_url $config[notify_url], amount [ total $amount, // 单位分必须整数 currency CNY, ], payer [ openid $openid, ], ]; // 生成签名并发送请求 $headers $this-buildV3Headers(POST, /v3/pay/transactions/jsapi, $body); $response $this-curlPost($url, json_encode($body), $headers); // 记录原始响应排查时用 Log::write(wechat_unified_order: . $response, pay); $result json_decode($response, true); if (isset($result[prepay_id])) { return $result[prepay_id]; } // 失败时把错误码和错误信息抛出来 throw new \Exception(微信下单失败: . ($result[message] ?? 未知错误)); } // 构造 V3 版本签名请求头 private function buildV3Headers($method, $urlPath, $body) { $config Config::get(pay.wechat); $timestamp time(); $nonceStr $this-generateNonceStr(); // 签名串格式HTTP方法\nURL路径\n时间戳\n随机串\n请求体\n $message $method . \n . $urlPath . \n . $timestamp . \n . $nonceStr . \n . json_encode($body) . \n; // 用商户私钥签名 openssl_sign($message, $signature, $config[private_key], sha256WithRSAEncryption); $sign base64_encode($signature); // 构造 Authorization 头 $token sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%d,serial_no%s, $config[mchid], $nonceStr, $sign, $timestamp, $config[serial_no] ); return [ Authorization: . $token, Accept: application/json, Content-Type: application/json, User-Agent: TP5-Pay-Client/1.0, ]; } private function generateNonceStr($length 32) { $chars abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789; $str ; for ($i 0; $i $length; $i) { $str . $chars[mt_rand(0, strlen($chars) - 1)]; } return $str; } private function curlPost($url, $data, $headers) { $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); $response curl_exec($ch); curl_close($ch); return $response; } }逻辑说明unifiedOrder方法接收订单号、金额、用户 openid 和商品描述构造 V3 要求的 JSON 请求体。金额字段total单位是分必须传整数传浮点数微信会直接返回参数错误。buildV3Headers是 V3 签名的核心签名串按「HTTP方法\nURL路径\n时间戳\n随机串\n请求体\n」拼接注意请求体后面也有一个换行符少一个\n签名就对不上。openssl_sign用商户私钥签名算法指定sha256WithRSAEncryption。Authorization头里serial_no是商户证书序列号在微信商户平台可以查到。参数说明$config[private_key]是商户私钥文件内容不是路径读取时用file_get_contents加载。$config[serial_no]是证书序列号不是商户号。notify_url必须是 HTTPS 且公网可访问本地开发时用内网穿透工具映射一个临时域名但注意回调地址不能带端口号以外的参数。3.2 异步回调验签与订单状态回写微信支付成功后会把结果 POST 到notify_urlV3 的回调报文是加密的需要先用 APIv3 密钥解密再验签。这一步是很多项目「支付成功但订单没变状态」的根因。?php // application/pay/controller/Notify.php namespace app\pay\controller; use think\Controller; use think\facade\Log; use app\pay\logic\WechatPay; class Notify extends Controller { public function wechat() { $raw file_get_contents(php://input); Log::write(wechat_notify_raw: . $raw, pay); $data json_decode($raw, true); if (!$data || !isset($data[resource])) { return json([code FAIL, message 报文格式错误]); } // 解密 resource 里的密文 $wechatPay new WechatPay(); $decrypted $wechatPay-decryptResource($data[resource]); if (!$decrypted) { return json([code FAIL, message 解密失败]); } Log::write(wechat_notify_decrypted: . $decrypted, pay); $result json_decode($decrypted, true); // 判断支付状态 if ($result[trade_state] ! SUCCESS) { return json([code SUCCESS, message OK]); // 非成功状态也返回成功避免重复通知 } // 根据 out_trade_no 查订单做幂等处理 $orderNo $result[out_trade_no]; $order db(order)-where(order_no, $orderNo)-find(); if (!$order) { return json([code FAIL, message 订单不存在]); } if ($order[status] 1) { return json([code SUCCESS, message OK]); // 已处理过直接返回成功 } // 更新订单状态记录微信交易号 db(order)-where(id, $order[id])-update([ status 1, pay_time time(), transaction_id $result[transaction_id], ]); return json([code SUCCESS, message OK]); } }逻辑说明回调入口先读取原始报文并写日志这是排查问题的第一手资料。V3 回调的resource字段是 AES-256-GCM 加密的需要用 APIv3 密钥解密。解密后拿到trade_state只有SUCCESS才处理订单。幂等判断很关键——微信会重复通知如果订单已经是支付成功状态直接返回成功不要重复更新。更新订单时记录微信的transaction_id对账时用得上。参数说明decryptResource方法内部用openssl_decrypt算法aes-256-gcm密钥是 APIv3 密钥iv和associated_data从resource的nonce和associated_data字段取。返回给微信的报文必须是{code:SUCCESS,message:OK}否则微信会按策略重试重试多次后停止并标记异常。注意回调接口不要做登录态校验微信服务器没有你的用户 session。所有安全校验靠签名和解密来保证。4. 支付宝支付接入当面付与手机网站支付的参数差异4.1 当面付扫码支付的下单与轮询支付宝当面付适合 PC 端展示二维码的场景接口是alipay.trade.precreate。和微信不同支付宝的签名走 RSA2参数用biz_content包裹业务参数。?php // application/pay/logic/AlipayPay.php namespace app\pay\logic; use think\facade\Config; use think\facade\Log; class AlipayPay { // 当面付下单返回二维码链接 public function precreate($orderNo, $amount, $subject) { $config Config::get(pay.alipay); // 公共参数 $params [ app_id $config[app_id], method alipay.trade.precreate, format JSON, charset utf-8, sign_type RSA2, timestamp date(Y-m-d H:i:s), version 1.0, notify_url $config[notify_url], ]; // 业务参数金额单位是元保留两位小数 $bizContent [ out_trade_no $orderNo, total_amount number_format($amount / 100, 2, ., ), subject $subject, ]; $params[biz_content] json_encode($bizContent, JSON_UNESCAPED_UNICODE); // 生成签名 $params[sign] $this-generateSign($params, $config[private_key]); // 发送请求 $response $this-curlPost(https://openapi.alipay.com/gateway.do, $params); Log::write(alipay_precreate: . $response, pay); $result json_decode($response, true); $responseNode str_replace(., _, $params[method]) . _response; if (isset($result[$responseNode][qr_code])) { return $result[$responseNode][qr_code]; } throw new \Exception(支付宝下单失败: . ($result[$responseNode][sub_msg] ?? 未知错误)); } // 生成 RSA2 签名 private function generateSign($params, $privateKey) { // 过滤空值和 sign 字段按 key 字典序排序 ksort($params); $signStr ; foreach ($params as $k $v) { if ($k sign || $v || $v null) { continue; } $signStr . $k . . $v . ; } $signStr rtrim($signStr, ); // 用私钥签名 openssl_sign($signStr, $signature, $privateKey, OPENSSL_ALGO_SHA256); return base64_encode($signature); } private function curlPost($url, $data) { $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); $response curl_exec($ch); curl_close($ch); return $response; } }逻辑说明支付宝的公共参数和业务参数分开业务参数放在biz_content里 JSON 编码。金额单位是元和微信的「分」不同这里容易搞混。签名前先ksort按 key 排序拼接成kvkv格式过滤空值和sign字段本身。openssl_sign用OPENSSL_ALGO_SHA256对应 RSA2。返回结果里qr_code是二维码链接前端拿到后生成二维码图片展示。参数说明total_amount用number_format保留两位小数传1和1.00支付宝都接受但传1.000会报参数错误。notify_url和微信一样要公网 HTTPS。app_id是支付宝开放平台的应用 ID不是商户号。4.2 手机网站支付与同步跳转手机网站支付接口是alipay.trade.wap.pay返回的是一个 HTML 表单需要自动提交跳转到支付宝收银台。和当面付的区别在于product_code参数和返回形式。?php // application/pay/logic/AlipayPay.php 补充方法 public function wapPay($orderNo, $amount, $subject, $returnUrl) { $config Config::get(pay.alipay); $params [ app_id $config[app_id], method alipay.trade.wap.pay, format JSON, charset utf-8, sign_type RSA2, timestamp date(Y-m-d H:i:s), version 1.0, notify_url $config[notify_url], return_url $returnUrl, // 同步跳转地址 ]; $bizContent [ out_trade_no $orderNo, total_amount number_format($amount / 100, 2, ., ), subject $subject, product_code QUICK_WAP_WAY, // 手机网站支付固定值 ]; $params[biz_content] json_encode($bizContent, JSON_UNESCAPED_UNICODE); $params[sign] $this-generateSign($params, $config[private_key]); // 构造自动提交的表单 $html form idalipay_form actionhttps://openapi.alipay.com/gateway.do methodPOST; foreach ($params as $k $v) { $html . input typehidden name . $k . value . htmlspecialchars($v) . ; } $html . /formscriptdocument.getElementById(alipay_form).submit();/script; return $html; }逻辑说明手机网站支付返回 HTML 表单前端直接输出这段 HTML 就会自动跳转到支付宝。product_code固定为QUICK_WAP_WAY这是和当面付的核心区别。return_url是支付完成后用户浏览器跳回的地址只用于展示结果订单状态仍以异步通知为准。参数说明return_url不能带自定义参数支付宝会原样跳回如果需要区分订单把订单号拼在路径里。同步跳转的 GET 参数里支付宝会带上out_trade_no和trade_no但不要依赖同步跳转改订单状态用户可能直接关掉页面。5. 避坑与排查双支付集成中最容易翻车的五个点5.1 签名失败但错误信息模糊现象微信返回401 Unauthorized或支付宝返回sign check fail但日志里看不出具体哪个参数错了。原因签名串拼接时多了或少了换行符、参数排序不对、编码不一致。微信 V3 的签名串对\n极其敏感支付宝的biz_content如果 JSON 编码时带了转义斜杠也会导致签名不一致。解决把签名前的原始串完整写到日志里和官方文档的示例逐字符对比。微信 V3 重点检查请求体后面的\n支付宝重点检查json_encode是否用了JSON_UNESCAPED_UNICODE和JSON_UNESCAPED_SLASHES。我一般会在签名方法里加一行Log::write(sign_str: . $signStr, pay)出问题时直接看这个串。5.2 回调地址 404 或 500现象支付成功但订单状态不变日志里没有回调记录。原因notify_url配置错误、路由未放行、HTTPS 证书不被信任、回调接口抛异常导致 500。解决先用 Postman 或 curl 手动 POST 一个模拟报文到回调地址确认路由能通。TP5 的路由如果开了强制路由模式需要在route.php里显式注册回调路由并设为POST方法。HTTPS 证书用正规 CA 签发的自签名证书微信和支付宝都会拒绝。回调接口里所有异常都要捕获并返回成功报文否则对方会一直重试。5.3 金额单位混淆导致少收或多收现象订单金额 1 元实际支付 100 元或 0.01 元。原因微信金额单位是分支付宝是元代码里没做转换或转换时除了 100 又乘了 100。解决在订单表里统一存「分」调用微信时直接传调用支付宝时除以 100 并number_format保留两位。所有金额计算用bcmath扩展避免浮点误差。我习惯在logic层加一个formatAmount方法根据支付渠道自动转换调用方不用关心单位。5.4 重复回调导致订单重复处理现象同一笔订单被多次更新库存被扣多次或者日志里出现多条相同交易号。原因微信和支付宝都会在未收到成功响应时重复通知间隔从几秒到几分钟不等。解决回调入口先查订单状态已支付成功的直接返回成功报文不做任何业务处理。更新订单时用where(status, 0)作为条件利用数据库行锁保证只有第一次更新生效。更稳妥的做法是加一个pay_notify_log表用transaction_id做唯一索引插入成功才处理业务。5.5 证书路径和权限问题现象微信支付报私钥文件不存在或证书序列号不匹配。原因证书文件路径用了相对路径TP5 的工作目录和预期不一致或者证书文件权限不对PHP 进程读不到。解决证书路径用app()-getRootPath() . extend/pay/wechat/apiclient_key.pem拼绝对路径。Linux 下证书文件权限设为 600属主和 PHP 进程用户一致。证书序列号在微信商户平台「API 安全」里查看复制时注意不要带空格。更新证书后记得同步更新serial_no配置。6. 进阶技巧用统一支付网关收敛两套逻辑跑通两套支付后我发现最麻烦的不是接入而是后续维护——每加一个支付渠道就要改一遍订单逻辑回调处理也各写各的。后来我抽了一个简单的支付网关层把渠道差异收敛到配置和适配器里订单模块只跟网关打交道。核心思路是定义一个PayGateway类对外暴露pay()、notify()、query()三个方法内部根据渠道标识路由到微信或支付宝的适配器。适配器实现统一接口返回统一结构。这样新增渠道时只需要加一个适配器类订单模块一行不用改。?php // application/pay/logic/PayGateway.php namespace app\pay\logic; class PayGateway { // 渠道适配器映射 private $adapters [ wechat WechatPay::class, alipay AlipayPay::class, ]; // 统一下单入口 public function pay($channel, $orderNo, $amount, $extra []) { if (!isset($this-adapters[$channel])) { throw new \Exception(不支持的支付渠道: . $channel); } $adapter new $this-adapters[$channel](); // 统一返回结构qr_code / redirect_html / prepay_params switch ($channel) { case wechat: $prepayId $adapter-unifiedOrder($orderNo, $amount, $extra[openid], $extra[desc]); return [type jsapi, prepay_id $prepayId]; case alipay: if ($extra[scene] wap) { return [type html, content $adapter-wapPay($orderNo, $amount, $extra[desc], $extra[return_url])]; } return [type qrcode, content $adapter-precreate($orderNo, $amount, $extra[desc])]; } } // 统一回调入口根据渠道分发 public function notify($channel) { $adapter new $this-adapters[$channel](); return $adapter-handleNotify(); } }逻辑说明pay()方法根据渠道标识实例化对应适配器把不同渠道的返回统一成typecontent结构前端根据type决定是拉起 JSAPI、展示二维码还是输出 HTML。notify()方法把回调处理也收敛控制器里只需要(new PayGateway())-notify(wechat)一行。适配器内部各自处理签名、解密、验签对外返回统一的订单更新结果。参数说明$extra数组承载渠道特有参数微信需要openid支付宝手机网站需要return_url和scene。新增渠道时在$adapters里加映射实现对应方法即可。这个结构不追求大而全够用就行过度设计反而增加理解成本。从那以后我每次接支付都强制先把回调日志和幂等判断写进去再写业务逻辑。支付这东西出问题时没有日志就是黑匣子有了日志至少能定位到是签名、网络还是业务代码的锅。希望帮到你。本文还有配套的精品资源点击获取