ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ThinkPHP5支付集成实战:微信V3与支付宝电脑网站支付避坑指南

ThinkPHP5支付集成实战:微信V3与支付宝电脑网站支付避坑指南 简介面向ThinkPHP5开发者的支付集成资料包围绕微信支付、支付宝支付及网银支付接口对接展开涵盖商户配置、SDK调用、支付流程与安全验证等关键环节适合需要快速接入在线支付的PHP开发者和技术团队。压缩包共2321个文件以php源码为主辅以data数据文件、配置文件、md文档、xml及yml配置总计4.76MB结构清晰便于按需查阅。已有924人学习下载资源内提供大量示例代码与配置模板可帮助理解统一下单、回调验签、支付二维码生成等核心流程还包含think命令行工具、测试脚本及证书样例方便本地调试与二次开发能有效缩短支付功能的集成周期。 去年年中接到一个电商项目的二次开发需求原本的支付模块只接了微信老板要求把支付宝也加上而且明确说框架是ThinkPHP5不能推倒重来。我当时心想这不就是两个支付SDK各自调一下的事嘛结果真正动手才发现微信支付从V2到V3的迁移、支付宝公众号支付和电脑网站支付的选择、回调验签的细节每一步都有坑。尤其是小程序微信支付V3对接时“无可用的平台证书”这个问题折腾了我整整一个下午。这篇就把我这次接入的完整过程、选型思路和踩坑记录整理出来给还在跟ThinkPHP5支付集成为伴的朋友一个参考。1. 动手前的关键决策v2还是v3电脑网站支付还是当面付先说结论新项目直接选微信支付V3支付宝优先看业务场景是电脑端还是手机端沙箱环境建议从头就挂上。我在接手这个项目时原代码里已经有一套微信支付V2的封装用的是老版SDK和XML接口。老板的需求是“把支付宝也接进来”但我看了一眼现状决定把微信支付也一起升级到V3。原因很简单微信支付V2的接口已经在逐步收紧新商户号默认开通的往往是V3权限而且V3的API统一使用JSON格式和RSA签名代码写起来比V2那套XML加MD5签名清爽太多。与其留着两套老代码不如一步到位。支付宝那边就有点讲究了。当时业务方说得很模糊只说“用户下单后能扫码付款就行”。我一开始图省事直接用支付宝的“当面付”接口生成二维码结果在电脑端浏览器里测试时发现当面付的QR Code在PC上需要用户用手机去扫体验上没问题但对账和退款流程反而不如电脑网站支付Alipay Trade Page Pay来得标准。后来我确认了业务场景是PC端商城下单果断切到电脑网站支付这样支付宝会直接返回一个form表单可以自动提交也可以手动渲染成二维码而且支持用户登录支付宝账号付款退款、查询都走标准接口后续维护省心很多。所以这里最核心的选型逻辑就一句话先确认用户是在手机里打开还是电脑上打开再定支付产品。手机上用手机网站支付WAP支付或App支付电脑上用电脑网站支付线下扫码才用当面付。别一上来就抄网上那些糊涂代码。除了产品选型还要顺便定好几个全局参数回调地址notify_url必须是外网可访问的HTTPS地址不能用IP不能用localhost订单号必须唯一同一订单号重复发起支付会直接报错金额单位统一为“分”微信和“元”支付宝两边的单位不一样封装时一定注意。2. 微信支付V3接入实操平台证书问题与签名流程微信支付V3的接入流程说起来很标准商户号、APIv3密钥、商户证书、平台证书四样东西凑齐就能调通。但实际操作时最容易卡住的就是“平台证书”。我当时遇到的报错原文是小程序微信支付v3对接 无可用的平台证书请在商户平台-api安全申请使用微信支付公钥。排查一圈后发现问题出在我用的微信支付SDK版本和证书下载方式不匹配。较新版本的SDK支持“微信支付公钥”也叫平台公钥但旧版本的SDK还在用老的“平台证书”逻辑。如果你的商户号已经升级到新版支付公钥体系老SDK去拉取证书就会提示“无可用平台证书”。解决方式有两种第一种把SDK升级到支持新公钥体系的版本然后在商户平台下载“微信支付公钥”不是平台证书配置时用这个公钥做验签。我后来就是用了这种方式具体用的是wechatpay-php这个官方推荐SDK。第二种如果你因某些原因不能升级SDK那就去商户平台手动下载“平台证书”并配置到本地但这种情况在新商户号里越来越少见了建议优先考虑第一种。配置好证书之后核心代码流程大概是这样的先用商户私钥生成Authorization请求头再去调用统一下单接口拿到prepay_id之后再拼接调起支付所需的参数。我在ThinkPHP5里的封装方式是这样的use WeChatPay\Builder; use WeChatPay\Crypto\Rsa; use WeChatPay\Util\PemUtil; class WechatPayService { private $merchantId; private $merchantSerial; private $merchantPrivateKey; private $wechatPayCertificate; private $appId; private $notifyUrl; public function __construct() { $config config(payment.wechat); $this-merchantId $config[merchant_id]; $this-merchantSerial $config[merchant_serial]; $this-merchantPrivateKey PemUtil::loadPrivateKey($config[merchant_private_key_path]); $this-wechatPayCertificate $config[wechat_pay_certificate_path]; $this-appId $config[app_id]; $this-notifyUrl $config[notify_url]; } public function createOrder($orderNo, $amount, $description) { $instance Builder::factory([ mchid $this-merchantId, serial $this-merchantSerial, privateKey $this-merchantPrivateKey, certs $this-wechatPayCertificate, ]); $response $instance-chain(v3/pay/transactions/native)-post([ json [ appid $this-appId, mchid $this-merchantId, description $description, out_trade_no $orderNo, notify_url $this-notifyUrl, amount [ total $amount, currency CNY, ], ], ]); $result json_decode($response-getBody()-getContents(), true); return $result[code_url] ?? ; } }注意这里我用的是native下单返回的code_url可以直接生成二维码。如果接小程序要换成v3/pay/transactions/jsapi并且需要再拼接小程序调起支付所需的参数。微信支付V3的签名逻辑是SDK内部处理的但你要理解它的原理不然排查问题时无从下手。简单说每个请求都要用商户私钥对“请求方法 请求路径 时间戳 随机串 请求体”做RSA签名然后放在HTTP头Authorization字段里发给微信服务器微信用平台公钥验签反之微信返回的响应和回调通知则是用平台私钥签名你这边用平台公钥去验签。这个双向验签机制保证了数据不会被中间人篡改。3. 支付宝电脑网站支付参数构造与沙箱联调技巧支付宝这边的接入比微信要“清爽”很多因为支付宝的SDK发展得比较稳定老接口兼容性好。我用的是alipay-sdk-php在ThinkPHP5里直接用Composer引入就行。电脑网站支付的核心调用逻辑如下use Alipay\EasySDK\Kernel\Factory; use Alipay\EasySDK\Kernel\Config; class AlipayService { private $config; public function __construct() { $this-config new Config(); $this-config-protocol https; $this-config-gatewayHost openapi.alipay.com; $this-config-signType RSA2; $this-config-appId config(payment.alipay.app_id); $this-config-merchantPrivateKey file_get_contents(config(payment.alipay.private_key_path)); $this-config-alipayPublicKey config(payment.alipay.alipay_public_key); $this-config-notifyUrl config(payment.alipay.notify_url); Factory::setOptions($this-config); } public function createPagePay($orderNo, $amount, $subject) { $result Factory::payment()-pagePay() -optional(passback_params, http_build_query([order_no $orderNo])) -batchSetRequiredParams([ out_trade_no $orderNo, total_amount $amount, subject $subject, product_code FAST_INSTANT_TRADE_PAY, ]) -get(); return $result-getBody(); } }这里有一个非常容易搞混的点total_amount的单位是“元”是字符串类型的金额比如0.01而不是整数分。很多从微信转过来的朋友在这里翻车传了一个整数分过去结果支付宝一直提示金额格式错误或者实际扣款金额不对。passback_params这个字段我建议一定用上。因为系统回调的时候支付宝只会返回out_trade_no和trade_no如果业务系统里需要用其他参数来定位订单比如渠道来源、用户ID就可以用passback_params塞进去回调时原样返回。注意它长度限制和字符合法性我这里用http_build_query序列化是安全的。沙箱联调这一步我强烈建议在写代码之前就先把沙箱环境配好。支付宝沙箱地址是openapi.alipaydev.com在支付宝开放平台的“沙箱环境”里可以拿到一套专用的AppID、应用私钥和应用公钥。沙箱环境里的买家账号、卖家账号都是官方提供的模拟账号可以无限充值测试退款、关闭订单这些操作非常方便。我在代码里是这样做沙箱和正式环境切换的public function __construct() { $isSandbox config(payment.alipay.sandbox); $this-config-gatewayHost $isSandbox ? openapi.alipaydev.com : openapi.alipay.com; }这样做的好处是本地开发和测试都用沙箱部署到生产环境时只要把配置项改掉代码完全不用动。再说一句关于“支付宝电脑网站支付如何只返回一个二维码链接”的热搜问题。很多人以为支付宝也会像微信native那样返回一个code_url让你自己生成二维码其实不是。电脑网站支付返回的是一个完整的HTML表单里面有自动提交的脚本。如果你确实只想得到一个二维码链接用来展示有两个变通办法第一个直接把这个HTML输出到页面让支付宝自动跳转到收银台这是官方推荐做法。第二个如果你是在一个不能自动跳转的场景里比如微信内嵌浏览器里要展示支付宝二维码可以解析这个表单里的action和biz_content参数把它们拼成一个URL然后再用二维码API生成二维码图片但这个做法本质上是让你自己去构造一个支付宝的收银台URL并不稳定不建议生产环境使用。就我个人的经验如果你真的需要“只返回一个二维码链接”这种需求那大概率你的场景是用户手上没有支付宝账号、只能用别人手机扫码代付。这种情况下不如直接考虑当面付接口当面付返回的就是一个纯链接方便你生成二维码。而电脑网站支付更合适的场景是“在当前浏览器里完成支付”。4. 回调处理与验签把好支付成功的最后一关支付回调是整个支付接入里最不能出错的地方因为这是钱真正到你账户的确认信号。我见过太多项目前面下单支付都调通了回调处理写得稀烂结果对账对不上、订单状态错乱甚至被人伪造回调刷单。微信支付V3的回调通知POST请求体的格式是{ id: EV-..., event_type: TRANSACTION.SUCCESS, resource_type: encrypt-resource, resource: { algorithm: AEAD_AES_256_GCM, ciphertext: ..., original_type: transaction, nonce: ..., associated_data: ... } }resource里的内容是加密的需要用APIv3密钥解密。在PHP SDK里解密逻辑已经封装好了但很多人解密失败的原因是把APIv3密钥和商户API密钥搞混。APIv3密钥是在商户平台“API安全”里设置的32位字符串而商户API密钥是V2时代的产物两者不一样。我见过有人把V2的32位密钥填到V3配置里结果解密出来的数据是乱的。解密之后你需要做三件事验签、校验金额、处理幂等。验签SDK会自动校验HTTP头里的签名确认这个请求是真的来自微信服务器。如果你自己手写签名校验要注意用平台证书或平台公钥验签而不是用商户私钥这个方向反了就会一直验签失败。校验金额把解密后的数据拿出来对比out_trade_no对应的订单金额和amount.total是否一致。注意amount.total单位是分而订单表里如果存的是元一定要转换后再比较。如果不一致必须当作支付失败处理绝不能直接更新订单状态。这一步是防止有人用小额订单号替换大额订单号之类的畸形操作。处理幂等同一个支付成功的通知微信可能会重试多次最多重试一定次数间隔递增所以你的回调逻辑必须能幂等处理。最简单的做法是在更新订单状态前先查一次订单当前状态如果已经是“已支付”直接返回成功标志不再重复更新。支付宝的回调处理逻辑也差不多只不过验签用的是支付宝公钥而且回调通知里的trade_status有多个值WAIT_BUYER_PAY、TRADE_SUCCESS、TRADE_FINISHED等。注意只有TRADE_SUCCESS和TRADE_FINISHED才表示支付成功WAIT_BUYER_PAY只是创建了交易但没付钱千万别当作成功处理。我踩过一个坑支付宝文档里说“TRADE_FINISHED在退款完成或交易完成时触发此时不能退款”但实际业务里大部分情况是TRADE_SUCCESS先到我一开始只判断了TRADE_SUCCESS结果某些情况下交易直接变成TRADE_FINISHED订单状态没有更新。后来统一改成这两个状态都视为成功才彻底解决问题。处理完业务逻辑后支付宝要求返回纯文本success不要JSON不要带任何HTML微信要返回{code:SUCCESS}。响应内容不对平台会一直重试通知导致日志刷屏。5. 实际踩坑记录证书过期、回调内网穿透与金额单位最后把我这次接入过程中遇到的几个比较折腾的问题完整记录下来给朋友们一个排查链路的参考。第一个是微信支付回调地址无法访问的问题。因为微信和支付宝的回调都必须让外网能访问到你服务器上的接口本地开发时就非常头疼。我推荐用内网穿透工具把本地的ThinkPHP5项目暴露到公网然后用平台提供的“模拟通知”功能直接触发回调这样本地就能断点调试回调逻辑效率极高。穿透工具的选型注意选稳定一点的不然回调请求断断续续排查起来很崩溃。第二个是证书过期问题。微信支付V3的商户证书有效期一般是5年但平台证书或者平台公钥有可能会自动轮换。SDK一般会自动下载并更新平台证书但如果你的服务器时间不准、或者把证书缓存到Redis里导致缓存没有及时更新就会出现突然“验签失败”的情况。我当时的处理方式是给缓存的证书设置一个合理的过期时间比如12小时并加一个手动刷新接口排查问题时直接调用刷新接口看是否能正常拉取新证书。第三个是回调时间与业务时间不一致的问题。微信和支付宝回调里的时间都是UTC时间比如2024-01-15T10:00:0008:00如果你直接存字符串数据库里排序和查询会很难受。我建议在入库前统一转成本地时间再存储或者在查询时再处理。这个不算大坑但很多人忽略了。第四个是金额类型问题。ThinkPHP5的数据库字段如果用decimal(10,2)查询出来是字符串用比较时PHP会自动转换一般问题不大但如果用强比较字符串“10.00”和浮点数10.0不相等就会导致金额校验失败。我的建议是回调里统一用bccomp函数比较金额字符串避免浮点精度问题。这一点在支付宝那边尤其重要因为支付宝传的金额就是字符串类型的元值比如“10.00”而微信那边是整数分两边单位不同封装一个统一的金额处理函数是非常必要的。第五个也是比较冷门但真实的坑支付宝沙箱环境的AppID和正式环境的AppID完全不一样如果你在代码里硬编码了AppID和密钥切换到正式环境时很可能漏改某个文件。我在项目里把所有支付配置都集中到了application/extra/payment.php然后在服务类初始化时统一读取这样至少能保证切换环境时只有一个入口。下面是我当时在ThinkPHP5里的配置结构return [ wechat [ merchant_id 你的商户号, merchant_serial 商户证书序列号, merchant_private_key_path ROOT_PATH . cert/apiclient_key.pem, wechat_pay_certificate_path ROOT_PATH . cert/wechatpay_cert.pem, app_id 你的AppID, notify_url https://yourdomain.com/payment/wechat/notify, ], alipay [ app_id 你的支付宝AppID, private_key_path ROOT_PATH . cert/alipay_private_key.pem, alipay_public_key 支付宝公钥字符串, notify_url https://yourdomain.com/payment/alipay/notify, sandbox false, ], ];配置文件里别放证书内容放证书路径即可这样证书更新时不需要改代码。证书文件权限记得设置成600或640避免被同服务器的其他用户读取。6. 封装层设计让微信和支付宝在业务代码里“无感”支付这块最影响长期维护体验的是你业务代码里调用的方式。如果业务层直接一个接口写微信、一个接口写支付宝后面要加新的支付方式或者调整费率计算就会非常痛苦。我在ThinkPHP5里做了一层简单的支付门面封装。业务层只需要知道一个统一的方法payment()-create($orderNo, $amount, $subject, $channel);其中$channel传wechat或者alipay底层根据渠道选择对应的服务类实现。这样的话下单逻辑、回调逻辑、退款逻辑全部统一入口后续增加新渠道比如银联时只需要新增一个类并注册进去。具体实现上可以用一个简单的工厂模式class PaymentFactory { public static function create($channel) { switch ($channel) { case wechat: return new WechatPayService(); case alipay: return new AlipayService(); default: throw new \InvalidArgumentException(Unsupported payment channel); } } }然后封装一个门面类供控制器调用class Payment { public static function create($orderNo, $amount, $subject, $channel) { $service PaymentFactory::create($channel); return $service-createOrder($orderNo, $amount, $subject); } }这样做还有一个额外的好处你在控制器里写“下单”逻辑时不用关心当前是微信还是支付宝只是替用户把支付入口生成好业务逻辑更清晰。关于回调入口我也建议直接做一个统一的控制器方法根据请求参数来识别是哪个渠道的回调再做渠道分发。微信的回调URL路径是/payment/wechat/notify支付宝是/payment/alipay/notify分开路径也方便在配置文件中直接配置无需额外判断。这套结构看起来不复杂但在实际项目中非常有价值——尤其是在后期订单对账、退款、账单导出时业务层只需要调用门面方法完全不用关心底层是哪家渠道。7. 实测稳定性从下单到回调全链路的逻辑串联讲完独立的模块我再用一个具体的订单流程把整个链路串一遍方便新手能有一个整体的“地图”。用户在商城里选好商品点“去支付”的时候前端会把订单编号和支付渠道传给后端。后端控制器先查订单是否存在、是否已经支付如果未支付就调用之前定义的Payment::create()方法。微信渠道返回的是一个二维码链接字符串我把它直接塞给前端的二维码组件用户用微信扫码完成支付支付宝渠道返回的是一个HTML表单我把它放到一个独立的页面里自动提交。用户扫码或者跳转到支付页面之后微信和支付宝的服务器会异步通知我们的回调接口。回调接口先做验签和解密或验签和解析参数再从数据库里查对应的订单校验金额、校验订单状态最后把订单标记成已支付并把支付流水号记录到订单表里。处理完成后向支付平台返回成功响应。整个链路里我认为最容易写错的地方在于“异步通知可能比页面跳转先到”这一点。用户支付成功后微信可能马上触发回调而你的页面还在“支付中”状态轮询。所以业务里不要用“页面跳转”来作为支付成功依据一切以回调更新订单状态为准。页面可以做一个定时轮询订单状态等到回调把订单改成已支付后页面才显示支付成功页。还有一个小细节订单表里一定要记录支付渠道的流水号微信的transaction_id、支付宝的trade_no这是后续退款的必需参数。如果你没存流水号退款时要通过商户订单号去查多一步查询不说还可能查不到比如历史订单在不同商户号下所以下单成功后的流水号必须落库。退款接口我建议也提前封装好。微信V3的退款接口是v3/refund/domestic/refunds支付宝是alipay.trade.refund两者都是异步通知退款结果。虽然项目第一次上线时可能没开放退款入口但提前留好接口后面运营提需求时就能直接用了。日志这一项千万别省。我在接入过程中把请求参数、响应参数、回调原始数据、验签结果、解密后的数据全部写入了日志文件排查问题时真的能帮你节省几个小时。日志建议分渠道、分类型存wechat_request、wechat_notify、alipay_request、alipay_notify这样出问题时能快速定位到是下单环节还是回调环节。另外ThinkPHP5默认的日志驱动是file如果你一台服务器上跑了好几个项目日志可能会串建议在配置里按项目区分目录或者用daily模式按天切分避免单个日志文件太大影响查询。最后再分享一个小经验上线前一定要把微信和支付宝的“沙箱模式”完整走一遍测试用例包括下单、支付成功、取消支付、超时关单、部分退款、全额退款、重复回调这七种场景。我当时写了一份测试清单每一项都验证通过之后才切正式环境上线当天就非常顺利一条工单都没接到。支付这个东西涉及的是真金白银前面测试多花几个小时后面能帮你省掉无数次深夜救火。这套接入流程整体下来从代码量来说不到一千行但踩过的坑和积累下来的心得确实不少。希望这篇笔记能帮你把ThinkPHP5的支付集成这条路走得更顺一点。本文还有配套的精品资源点击获取
返回列表