简介:面向使用 ThinkPHP5 构建电商或在线服务、需要接入微信支付与支付宝支付的 PHP 开发者,这份 rar 压缩包给出可参考的支付集成工程与配套代码。内容围绕商户参数配置、统一下单与预支付会话标识生成、回调通知处理、签名验证及敏感数据传输等关键环节展开,并顺带提及网银支付的对接思路,适合具备一定框架基础、希望减少从零调试成本的中级开发者。包内共 2321 个文件,以 php 源码为主体,辅以 data 数据文件、phpt 测试用例、md 说明、xml 与 yml 配置、json 及 dist 构建产物等,整体约 4.76MB,目录层次较完整,便于按模块查看实现。已有 927 人学习下载。读者可从中获取支付接口的参数组织、请求响应封装、回调验签与异常处理等可复用代码片段,也能对照微信、支付宝两套流程梳理差异,为实际项目联调和排错提供参照。
1. 支付接入这件事:先把“能付钱”拆成四段可验证的状态
很多 ThinkPHP5 项目第一次接支付,卡点不在代码写不出来,而在状态对不上。用户付了钱、订单还是待支付;或者本地回调通了,一上线就静默失败。我一般把整个接入拆成四个可独立验证的状态:下单请求发出去了、支付参数正确拿到了、异步通知验签通过了、订单状态幂等更新了。这四段任何一段糊弄过去,后面都是血泪经验。这篇围绕 TP5 下的支付集成包展开,讲清楚微信支付与支付宝两条链路的配置、签名、回调验签与排错,适合手里有现成商城、报名、会员续费类 TP5 项目、准备自己动手接支付接口的开发者。看完你至少能判断一件事:手上这份代码,是能直接跑通的骨架,还是只能当参考的半成品。
2. 两条链路的差异:为什么不能用一个封装糊住微信和支付宝
微信支付和支付宝支付的接口风格差异很大,硬套一层“统一支付类”往往在回调验签环节翻车。先把这条边界划清楚,后面的目录结构和配置才站得住。
2.1 签名机制不同,决定了配置项必须分开存
微信支付 v2 用的是 MD5 或 HMAC-SHA256,参与签名的字段要按 ASCII 码从小到大排序,拼成k=v&k=v形式再追加密钥,空值和sign字段必须剔除。支付宝走的是 RSA2,私钥签名、公钥验签,参数集合相对固定,但要注意sign_type和编码。两者差的不只是算法,是整条参数处理流程。
常见的错误做法是写一个Pay::sign($data)然后按type分支判断,看起来统一,实际每次加字段都要改核心类。我的做法是每个渠道一个独立的 Service 类,各自持有自己的配置数组,公共的订单处理逻辑放在上层。这样微信那边改 API 版本不会波及支付宝,支付宝换密钥也不用来回翻微信的代码。
2.2 异步通知的到达方式不同,回调地址要分开设计
微信的notify_url必须公网可达、不能带参数串、不能用内网地址;支付宝的notify_url同理但返回体必须是纯文本success,微信要返回 XML 结构。两者都不能依赖用户浏览器跳转,return_url只负责展示,真正的发货依据只能是异步通知。
在 TP5 里,这意味你至少需要三个路由入口:微信统一下单接口、微信回调、支付宝回调(支付宝下单可以在服务端做,也可以跳转)。路由最好不要复用,回调地址一旦写死到商户后台配置里,后期改动成本很高,一开始就按域名/pay/wechatNotify、域名/pay/aliNotify这种清晰结构规划好。
2.3 证书与密钥的存放位置,TP5 项目里最容易随手放错
微信退款、企业付款这类接口需要 API 证书(apiclient_cert.pem与apiclient_key.pem),支付宝当面付、转账类接口需要应用私钥。这些文件绝不能放在public/目录下,否则一个路径猜测就能下载走。TP5 的惯例是放到EXTEND_PATH下或者项目根目录外的独立目录,通过配置里的绝对路径引用。
// config/pay.php —— 两条链路的配置分开写,敏感路径走绝对路径 return [ 'wechat' => [ 'appid' => env('WX_APPID', ''), // 公众号/小程序 appid 'mch_id' => env('WX_MCH_ID', ''), // 商户号 'key' => env('WX_KEY', ''), // v2 密钥,32 位 'notify_url' => 'https://your-domain.com/pay/wechatNotify', 'cert_path' => '/data/certs/wechat/apiclient_cert.pem', // 退款才需要 'key_path' => '/data/certs/wechat/apiclient_key.pem', ], 'alipay' => [ 'app_id' => env('ALI_APP_ID', ''), 'private_key' => '/data/certs/alipay/app_private_key.pem', 'public_key' => '/data/certs/alipay/alipay_public_key.pem', 'notify_url' => 'https://your-domain.com/pay/aliNotify', 'return_url' => 'https://your-domain.com/pay/aliReturn', 'gateway' => 'https://openapi.alipay.com/gateway.do', ], ];这段配置的逻辑是:密钥类信息一律走环境变量,文件路径用绝对路径,避免 TP5 在不同部署目录下ROOT_PATH解析出偏差。notify_url必须是 HTTPS 且公网可访问,本地调试需要内网穿透工具,但穿透地址会变,所以回调地址不要在代码里写死成调试地址。参数上要特别注意微信的key是 32 位、不能含特殊字符,支付宝私钥文件必须是 PKCS8 格式且不带密码——这两条是新手最常踩的坑,报错信息通常只告诉你“签名错误”,不会告诉你格式不对。
3. 从下单到回调:TP5 支付集成包的落地步骤
这一章按“装包 → 配置 → 下单 → 回调 → 对账”五步走,每一步都给到可以直接抄的代码结构和该改哪里。
3.1 依赖引入与目录结构规划
这类集成包通常是基于官方 SDK 的二次封装,composer 安装后落在vendor/下,但业务代码不应直接调用 vendor 里的类,否则换版本会很痛。我一般在自己项目的application/common/service/pay/下建三层:WechatService、AliService、PayFactory,分别承接渠道逻辑与工厂分发。
# 安装官方 SDK(以常见命名空间为例,实际以资源的 composer.json 为准) composer require wechatpay/wechatpay alipay/alipay-sdk-php # 确认自动加载生效 php think version执行后要确认vendor/autoload.php已被 TP5 的入口文件加载,php think version能正常输出版本说明框架本身没问题。如果安装后提示找不到类,八成是 composer 的autoload没有刷新,执行一次composer dump-autoload。这一步不涉及支付逻辑,但它是后面所有报错的源头之一,先排除掉。
3.2 微信统一下单:签名拼装与参数校验
微信统一下单的核心是把业务参数转成 XML(v2)或 JSON(v3),签名前剔除空值、按 key 排序、拼 key 加密。这里给一个可复用的签名方法,注意它对空值和sign字段的处理。
// application/common/service/pay/WechatService.php public function sign(array $params, string $key): string { // 1. 剔除签名字段本身与空值 unset($params['sign']); $params = array_filter($params, function ($v) { return $v !== '' && $v !== null; }); // 2. 按 ASCII 升序排列键名 ksort($params); // 3. 拼接成 k=v&k=v 并追加密钥 $str = urldecode(http_build_query($params)) . '&key=' . $key; // 4. MD5 后转大写 return strtoupper(md5($str)); }http_build_query默认会做 URL 编码,而微信签名要求原始值,所以加urldecode是必须的,少了这一步带中文商品名或带空格的价格就会签错。array_filter的回调里排除空字符串和 null,但不能简单地用array_filter($params),因为那样会把0和'0'也过滤掉,金额字段一旦是 0 元就丢参数了。签名结果转大写也是微信 v2 的硬要求,小写签名一律验签失败。参数层面,total_fee单位是分、必须是整数,out_trade_no长度不超过 32 位且不能重复,trade_type取NATIVE、JSAPI、APP等值时要配合不同的openid或product_id。
3.3 支付宝下单:RSA2 签名与网关调用
支付宝的参数集合更规整,但签名流程要严格区分“待签名字符串”和“请求参数串”。待签名内容是按 key 排序后拼成的k=v&k=v,不包含sign,也不做 URL 编码;请求时才统一编码。
// application/common/service/pay/AliService.php public function buildParams(array $bizContent): array { $params = [ 'app_id' => $this->config['app_id'], 'method' => 'alipay.trade.page.pay', 'format' => 'JSON', 'charset' => 'utf-8', 'sign_type' => 'RSA2', 'timestamp' => date('Y-m-d H:i:s'), 'version' => '1.0', 'notify_url' => $this->config['notify_url'], 'return_url' => $this->config['return_url'], 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE), ]; ksort($params); // 待签名串:原始值直接拼接,不做 URL 编码 $signStr = urldecode(http_build_query($params)); $params['sign'] = $this->rsaSign($signStr); return $params; }biz_content要保证中文不被转义,所以带JSON_UNESCAPED_UNICODE。timestamp必须用服务器时间且格式固定,服务器时区不对会导致支付宝返回“时间戳格式错误”。私钥在rsaSign里用openssl_sign处理,算法常量选OPENSSL_ALGO_SHA256对应 RSA2。这里最容易出错的是私钥格式:从开放平台下载的往往是 PKCS1,而 PHP 的openssl_pkey_get_private对两种格式兼容性不同,稳妥做法是先转成 PKCS8 并存成独立文件。
3.4 异步通知的验签与幂等落库
回调入口要做四件事:读原始输入、验签、校验金额与订单号、幂等更新。微信的输入是 XML,支付宝走$_POST,但为了兼容application/x-www-form-urlencoded与异常编码,我一般直接读php://input再解析。
// application/index/controller/Pay.php 回调处理方法 public function wechatNotify() { $raw = file_get_contents('php://input'); $data = $this->xmlToArray($raw); // 1. 验签:重算签名与回调签名比对 if (!$this->wechatService->verifySign($data)) { return $this->xmlReply('FAIL', 'sign error'); } // 2. 校验业务:订单号存在、金额一致、交易状态为 SUCCESS $order = Db::name('order')->where('order_no', $data['out_trade_no'])->find(); if (!$order || $order['pay_amount'] != $data['total_fee']) { return $this->xmlReply('FAIL', 'order mismatch'); } // 3. 幂等:只有待支付状态才更新,重复通知直接返回成功 if ($order['status'] == 0) { Db::name('order')->where('id', $order['id'])->update([ 'status' => 1, 'paid_at' => time(), 'trade_no' => $data['transaction_id'], ]); } return $this->xmlReply('SUCCESS', 'OK'); }逻辑说明:验签放在最前面,且失败也要返回微信可识别的 XML,否则微信会持续重推;订单校验里金额用total_fee(分)与本地存的支付金额对齐,不要拿商品原价去比,否则有优惠券的订单永远对不上;幂等判断用状态位做前置条件,可避免并发重复通知导致重复发货。参数上,transaction_id是微信侧流水号,建议单独存一列以便对账查询,attach字段可以用来带自定义业务标记。返回体必须是<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>结构,纯文本success微信不认。
3.5 对账与日志:把每次请求都留成可回放的黑匣子
支付问题一旦发生,没有日志就只能靠猜。我习惯在 Service 层入口和回调入口各记一条带唯一 trace 的日志,内容包含请求入参(脱敏后)、返回码、耗时,落盘到按天分目录。
// 记录到 runtime/paylog/2025-01-01.log 一类的路径 Log::write(sprintf( '[%s] channel=%s out_trade_no=%s code=%s cost=%dms raw=%s', date('Y-m-d H:i:s'), $channel, $orderNo, $result['code'] ?? 'unknown', $cost, json_encode($result, JSON_UNESCAPED_UNICODE) ), 'pay');日志里不要写完整密钥和用户敏感信息,返回体里sign也可以只截前 8 位。耗时统计的意义在于区分“网络超时”和“返错”,前者通常需要重试策略,后者要看返回码具体含义。对账环节可以每天定时拉取账单,用transaction_id与本地trade_no做匹配,差异订单单独标记出来人工处理,而不是自动改状态。
4. 避坑清单:签名失败、回调丢失与状态不一致的高频现场
这一章是我在实际项目里反复遇到过的几类问题,按“现象 → 原因 → 解决”整理,照着对就能省下大量翻文档的时间。
4.1 现象:支付成功但订单一直待支付
原因一般是异步通知根本没到达,或者到达了但验签失败返回了错误结构。先看回调入口有没有日志,如果连日志都没有,说明请求没进来,检查notify_url是否公网可达、是否是 HTTPS、是否被防火墙拦下;如果有日志但状态没变,看验签结果和订单校验分支,常见是金额比较用了浮点导致精度不等,或者订单号在回调前已被其他流程改掉。
解决上,回调入口的日志必须打在验签之前,先确认请求到了再看业务。金额比较统一转成整数分比较,订单号做一次规范化(去掉前后空格)。另外注意微信的回调可能连续推多次,本地更新逻辑必须幂等。
4.2 现象:微信统一下单返回“签名错误”
原因集中在三处:一是参数里有空值未剔除,二是金额字段带了小数点或单位用了元,三是密钥前后有空格。网络传输或复制粘贴带进来的空格肉眼很难发现,但签名会直接不对。
解决:在拼接签名字符串前打印一次ksort后的参数列表,排除掉空值字段;total_fee强制intval转换并确认单位是分;配置项在读取时trim一次。如果还是不对,拿微信官方文档里的签名校验工具对一遍参数,别凭感觉猜。
4.3 现象:支付宝返回“验签失败”但商户后台显示交易成功
这多半发生在回调处理里:支付宝回调的验签用的是支付宝公钥,不是应用公钥,两者很容易在配置里搞混。另外回调参数中sign和sign_type要剔除后再验签,且待验签串的拼接规则与请求签名一致。
解决:确认配置里public_key是开放平台提供的支付宝公钥,而不是自己生成的应用公钥;验签时把参数按 key 排序、剔除sign和sign_type,用原始值拼接后做 RSA 验签。验签不通过的请求一律不更新订单,但要在日志里记录完整入参以便追溯。
4.4 现象:本地调试一切正常,上线后回调 404
原因是路由与伪静态配置不一致,或者回调地址带上了调试时的端口和路径前缀。TP5 的回调路由如果在route.php里定义了但没关掉强制路由,实际访问路径可能和预期差一层。
解决:上线前用公网地址直接curl回调入口,确认返回结构正确;检查route.php中回调路由是否排在通配路由之前;如果用了 URL 重写,确认nginx配置里把index.php的 PATH_INFO 正确透传。回调地址不要带?参数,微信侧会做校验。
5. 进阶验证:用一个自建订单沙盘把支付链路跑成可回归的流程
支付接入真正难的不是第一次调通,而是每次改代码后还能确认没坏。我后来的习惯是搭一个极简沙盘:本地建一张测试订单表,用固定的假订单号、固定金额(1 分)跑完整链路,回调部分用脚本模拟推送,这样不依赖真实用户也能回归验证。
// 沙盘脚本:模拟一次微信回调,验证验签与幂等 $payload = [ 'return_code' => 'SUCCESS', 'out_trade_no' => 'TEST20250101001', 'transaction_id' => '4200000000202501010000000001', 'total_fee' => 1, 'result_code' => 'SUCCESS', ]; $payload['sign'] = $wechatService->sign($payload, $config['key']); // 用 curl 打到本地回调入口,重复两次验证幂等这个脚本的意义在于把“回调验证”从人工操作变成可重复执行的步骤。第一次调用后检查订单是否从待支付变成已支付,第二次调用检查状态是否保持不变、日志里是否出现“重复通知”。参数设计上金额固定 1 分,是为了避免沙盘数据污染真实对账,订单号用固定前缀便于定期清理。
验证方法还包含签名一致性检查:同一组参数连续签两次结果必须完全相同,如果不同说明拼接过程受到了时间或随机因素影响;以及回调返回体结构检查,微信必须是 XML、支付宝必须是纯文本success,用curl -i看响应头也能顺带确认 HTTP 状态码是 200。
| 验证项 | 期望结果 | 不通过的常见原因 |
|---|---|---|
| 同一参数两次签名 | 结果完全一致 | 参数里混入时间戳或随机数 |
| 回调重复推送 | 订单状态只变一次 | 缺少状态位幂等判断 |
| 金额校验 | 分单位整数比较 | 浮点转换精度丢失 |
| 返回体结构 | 微信 XML / 支付宝 success | 返回了 JSON 或空字符串 |
从那以后我每次改支付相关代码,都强制把沙盘脚本跑一遍再看日志,哪怕只改了一个字段名。支付这条链路的后悔药不多,能提前用可回归的流程兜住的,就别指望上线后再排查。希望帮到你。
本文还有配套的精品资源,点击获取