简介面向使用 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 码从小到大排序拼成kvkv形式再追加密钥空值和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 微信统一下单签名拼装与参数校验微信统一下单的核心是把业务参数转成 XMLv2或 JSONv3签名前剔除空值、按 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. 拼接成 kvkv 并追加密钥 $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 排序后拼成的kvkv不包含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字段可以用来带自定义业务标记。返回体必须是xmlreturn_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 或空字符串从那以后我每次改支付相关代码都强制把沙盘脚本跑一遍再看日志哪怕只改了一个字段名。支付这条链路的后悔药不多能提前用可回归的流程兜住的就别指望上线后再排查。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?