十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

易宝支付PHP SDK对接实战:从下单到回调验签的完整指南

易宝支付PHP SDK对接实战:从下单到回调验签的完整指南 1. 为什么我在2023年又回头研究易宝支付SDK先说个背景。我手里有个老项目跑了好几年的PHP电商系统一直用的是某家第三方支付。去年那家支付公司突然调整了结算政策手续费涨了不说T1到账变成T3财务那边先炸了。老板让我尽快对接一家新支付渠道对比了一圈易宝支付的资质、费率、结算周期都合适而且它家的PHP SDK虽然官方文档写得不够时髦但胜在接口稳定、坑少社区里也有大量踩坑记录可以参考。对接之前我其实有点犹豫——易宝支付是老牌支付公司了但市面上最新的那批支付SDK要么是支付宝、微信那种国民级应用要么是一些聚合支付平台。易宝的PHP SDK网上的教程大多停留在2017、2018年很多接口签名方式、回调参数的用法都已经更新了。这意味着我不能直接照抄老代码得重新读一遍官方文档、核对最新的接口报文规范。这篇文章不是官方文档的复读机而是把我从环境准备、SDK下载、接口联调、回调验签到上线排错的全过程整理出来。如果你也是做PHP项目、正准备对接易宝支付或者只是想了解一套支付SDK从零到上线到底要趟多少坑这篇应该能帮你省下不少时间。先说清楚几个基础概念易宝支付Yeepay目前主推的接口分为老版“一键支付”接口和新版“聚合支付/无卡支付”接口两者在SDK包结构、签名方式、回调字段上差别很大。本文以新版商用接口为主老版代码只会作为对比参考。另外整个对接流程涉及商户号、密钥、回调地址三个最关键的东西后面会反复提到请务必提前和易宝的商务确认清楚。2. SDK选型与对接前必备的环境清单2.1 先搞清楚你需要哪个SDK版本易宝支付的开放平台文档中心其实没有提供一个像“easy-pay-php-sdk”这样的大一统包。你在官方仓库或者Composer上能搜到的是按接口维度拆分的模块比如支付产品类银行卡支付、扫码支付、H5支付、App支付、公众号支付结算类单笔结算查询、批量结算退款类单笔退款、退款查询对账单类日账单下载。我做的是PC端商城用户主要用支付宝、微信扫码支付以及银行卡快捷支付所以选了“扫码支付”和“银行卡支付”两个模块。如果你做的是App内支付需要额外引入App支付SDK如果是微信公众号里发起支付则走JSAPI支付。每一类接口的公共报文头、加密方式和回调字段基本一致但业务字段各有差异选型阶段别贪多按实际业务需求来就好。这里提醒一句先看易宝开放平台的“产品接入文档”里当前标注的版本号。有些老的博客文章写的版本已经被下线了直接照着做会报“签名验证失败”或者“接口地址404”。我这次对接时文档首页明确标注了老版“一键支付”仅支持存量商户新商户必须走新版“聚合支付”接口。千万不要被网上一些“通用教程”误导。2.2 必需的账号资质、密钥和三张表对接前请先向易宝商务申请好以下东西否则代码写了一半会被卡住商户号通常是数字串类似 1001XXXXXXXX收款账号对公账户用来接收结算款商户密钥用于生成签名一般分为MD5密钥和RSA密钥两种新版接口以RSA为主回调地址你的服务器上用来接收支付结果通知的URL必须是公网可访问的HTTPS地址证书文件部分接口需要比如退款接口部分商户需要提供API证书。建议在项目根目录下创建一个config/payment.php文件把商户号、密钥、回调地址集中管理不要散落到多个类文件里。我已经吃过亏之前把密钥写死在Controller里后来换密钥时改得头皮发麻。配置项示例值说明merchantNo1001XXXXXX商户号易宝后台可查rsaPrivateKey一串PEM格式私钥用于请求报文加签yeepayPublicKey一串PEM格式公钥用于验证易宝回调签名callbackUrlhttps://api.example.com/pay/callback支付结果异步通知地址2.3 PHP环境要求与Composer安装易宝新版SDK要求PHP 5.6以上建议用PHP 7.2因为很多老项目的服务器还停留在PHP 5.6新版SDK虽然能跑但官方已经在代码里用了部分新语法低版本容易踩语法兼容的坑。安装有两种方式通过Composer直接引入官方包从官方文档下载SDK压缩包手动放到项目里。我推荐Composer方式方便后续升级和依赖管理。在项目根目录执行composer require yeepay/php-sdk如果没有Composer可以到易宝开放平台下载SDK压缩包解压后放到ext/yeepay下然后在代码里用require_once引入关键的入口文件。有些老项目没做Composer自动加载手动引入也没问题但要注意路径大小写和命名空间易宝SDK的命名空间有的是YeePay有的是Yeepay大小写不一致会导致autoload失败这类问题排查起来比较隐蔽。3. 支付下单接口对接核心请求与签名逻辑3.1 报文结构请求头、业务参数、签名新版易宝支付接口的大致报文结构如下无论哪个产品都是这个骨架{ merchantNo: 1001XXXXXX, token: 用于身份识别, version: 1.0, timestamp: 20230815120000, sign: 对整个业务报文生成的签名串, bizContent: { orderId: 20230815001, amount: 100, productName: 测试商品, callbackUrl: https://api.example.com/pay/callback } }实际发送时SDK内部会将bizContent序列化成JSON字符串放到外层报文的bizContent字段里然后对整个业务报文加签。加签算法一般是将 bizContent 里的所有参数按照ASCII码升序排列用keyvaluekeyvalue拼接成待签名字符串末尾拼接商户密钥使用 MD5 或 RSA 签名生成 sign 字段。这个过程我不建议自己手工实现直接调用SDK封装的buildRequest方法即可。但你要理解签名原理因为后面排查回调失败时90%的问题都出在“参数排序不一致”或“拼接串多了空格”上。3.2 一个可落地的扫码支付下单示例以扫码支付为例SDK调用流程可以简化为use Yeepay\Pay\Request\ScanPayRequest; $request new ScanPayRequest(); $request-setOrderId(20230815001); $request-setAmount(100); // 单位分 $request-setProductName(demo商品); $request-setCallbackUrl($config[callbackUrl]); $client new \Yeepay\Pay\Client($config); $response $client-execute($request); // 返回的响应里有二维码地址或二维码内容 $qrcodeUrl $response[qrcodeUrl]; // 前端页面用二维码生成库渲染这个值即可需要注意几个细节金额单位一律是“分”不是“元”。我第一版代码传了“100.00”支付网关返回“金额格式错误”排查半天才想起来这茬。订单号要保证唯一建议用date(YmdHis).rand(1000,9999)或者直接上雪花ID。重复订单号会导致下单失败或者覆盖原订单状态。callbackUrl不要带中文参数有些老版本的支付网关对URL参数编码不友好最好保持纯英文和数字。3.3 为什么下单接口返回了code0000但页面没跳转这是一个特别容易让人困惑的点下单请求返回成功但用户没有看到支付页面或二维码。可能原因有几种扫码支付接口返回的是二维码内容QRCode字符串不是二维码图片URL需要前端自己生成二维码图片后端如果直接把字符串输出到浏览器用户看到的是一堆乱码或纯字符H5支付接口需要重定向到特定URL但开发环境回调地址用了localhost易宝网关无法访问导致跳转后报错下单时没有传deviceType字段某些渠道会默认按PC端处理手机上打开PC端二维码会异常。建议先拿易宝官方提供的测试工具模拟一次完整请求确认返回的字段结构是否符合预期再对接前端。4. 回调通知处理验签、解析、更新订单与幂等4.1 回调的两种模式服务器异步通知 vs 前端同步跳转支付完成后易宝会往你配置的callbackUrl发送服务器异步通知也叫服务端回调这是最可靠的官方信号。同时用户的浏览器也会跳转回你配置的returnUrl这是页面同步跳转仅用于展示结果不能依赖它去改订单状态。我遇到不少新手把returnUrl当成回调来用用户在支付成功后看到页面结果订单状态一直没更新。正确姿势是在returnUrl页面里只做“支付结果展示”比如显示“支付成功请等待发货”在callbackUrl里完成订单状态更新、库存扣减、余额变动等核心业务逻辑为了让用户体验更好可以在returnUrl页面加一个轮询接口查询订单真实状态后再跳转。4.2 回调数据的验签流程易宝的异步通知回调报文是POST请求除了业务参数之外关键字段是sign和signType。正确验签流程如下接收POST原始报文去掉sign、signType以及空值的参数将剩余参数按ASCII码升序排序拼接成keyvaluekeyvalue格式字符串使用易宝公钥或MD5密钥验证签名验签通过后再校验商户号、订单号、金额是否与本地记录一致。SDK里一般会提供一个verifyCallback方法。我这次对接时发现官方SDK的验签方法有个小坑它对callbackUrl参数做了URL解码而我在下单时用urlencode处理过回调地址结果验签时字符串对不上。后来我在配置回调地址时统一用原始URL不在下单前编码问题解决。4.3 业务处理时的幂等性与并发安全支付回调是一个高并发、可重复通知的请求。易宝服务器在没收到你的成功应答时会按一定频率重复发送回调比如间隔1分钟、5分钟、30分钟。所以回调处理逻辑必须满足幂等先查本地订单表中是否有该orderId如果订单状态已经是“已支付”直接返回成功字符串不再重复处理只有订单状态为“待支付”时才执行更新逻辑更新订单状态时务必加行锁或者使用“乐观锁”机制避免两个并发请求同时进入。一个简单示例$order OrderModel::lockForUpdate()-find($orderId); if ($order-status OrderModel::STATUS_PAID) { return SUCCESS; } $order-status OrderModel::STATUS_PAID; $order-trade_no $tradeNo; $order-paid_at date(Y-m-d H:i:s); $order-save();4.4 回调响应的固定格式别返回JSON易宝不认回调处理完成后需要在响应体里输出易宝要求的成功标识。新版易宝文档要求返回纯文本字符串一般是SUCCESS失败返回FAIL或其他错误信息。有个朋友对接时习惯性在接口里返回{code:0,msg:success}这种JSON结果易宝一直重发回调把他的库存活活扣成了负数。因为易宝只认SUCCESS这个固定字符串其他任何内容都视为处理失败。用ThinkPHP或Laravel框架时控制器里如果设置了自动JSON序列化或统一响应格式记得在回调方法里改成手动echo SUCCESS;并exit;不要让框架拦截掉。5. 掉单问题与常见报错排查实战5.1 场景一用户支付成功但订单未更新这个现象是钱进了商户后台本地订单还是待支付。查了半天最经典的三个原因回调地址填的不是公网地址易宝服务器根本访问不到回调处理中验签失败没有正确输出SUCCESS易宝多次重发也都失败回调处理中出现了PHP报错比如数据库连接失败或字段不存在导致响应非SUCCESS。排查手段在回调方法入口处先写一个独立的日志文件比如error_log(callback received: .json_encode($input), 3, /tmp/yeepay_callback.log);用第三方工具如Postwoman、Postman配合内网穿透手动构造一次回调看能否打通查看易宝商户后台的“交易通知记录”里面能看到每次回调的时间、内容、返回值。5.2 场景二下单就报“商户不存在或状态异常”这个报错绝大部分是配置问题。检查四点商户号是否填错有些商户号包含字母我一度以为是纯数字实际有前缀是否在易宝开放平台后台完成了支付产品的开通没开通就调用下单接口会直接报错测试环境和生产环境是否用了不同的商户号代码里配的是测试号结果用生产号下单也会出现这种提示商户号和密钥是否配套同一个商户号下的密钥才是对应的混着用会验签失败。5.3 场景三签名验证失败换一下密钥就好签名失败是支付对接里最头疼的报错没有之一。它背后的原因五花八门但最常见的就这几类可能原因定位方法待签名字符串拼接顺序错误将SDK里的拼接代码和官方文档逐字符比对尤其是空值、null、数组序列化格式密钥配置多了空格或换行打印密钥内容检查PEM首尾是否有意外的不可见字符charset编码不一致易宝要求UTF-8项目里如果有其他编码有可能导致中文参数签名不一致回调地址在签名时是解码前和解码后的区别统一约定不下单前URL编码使用了错误的密钥版本新版接口用RSA老版可能用MD5混用必失败排查签名问题有一个笨办法但很有效直接把SDK里生成好的待签名字符串monolog打出来和易宝开放平台的“签名工具”生成的字符串做一次逐字对比马上能看出差异。大部分时候是拼接时少了一些固定字段比如version和timestamp没参与签名。5.4 场景四回调验签成功但订单金额对不上这种问题的本质是请求被黑客伪造或者用户利用了你业务逻辑的漏洞又或者你下单时金额计算有误。验签通过只代表数据确实来自易宝不代表业务数据一定正确。后端在更新订单状态前还要校验回调里的orderId是否存在于本地订单表回调里的amount是否等于本地订单的应付金额回调里的merchantNo是否等于当前商户号如果商品有限购或库存逻辑还要校验订单状态合法性。有些项目的订单金额在用户下单后还会变更比如优惠券、运费修改这时候更要严格比对否则可能被改单攻击。6. 测试环境到生产环境的迁移要点6.1 测试环境用真实支付还是沙盒模拟易宝提供了沙箱测试环境但沙箱里的支付行为不一定能完全模拟真实支付通道尤其是某些银行卡渠道沙箱可能只返回“成功”状态不会走真实的银行扣款。我的做法是先用沙箱环境跑通下单、回调的完整流程再用小额真实支付比如1分钱或1元验证生产环境的回调链路上线初期设置“支付成功通知到钉钉/企业微信群”实时监控交易状态。真实小额支付测试要注意不要频繁下单又退款否则商户号容易被风控盯上。测试订单用完后要么直接发货要么走正常的退款接口。6.2 日志与监控体系怎么搭支付模块最怕的是“出了问题无迹可寻”。上线前建议做好四层日志请求日志记录每一次下单请求的完整报文、响应报文和耗时回调日志记录每次回调的原始报文、验签结果、处理结果业务日志记录订单状态变更前后的值以及操作人、操作时间异常日志捕获所有Exception并记录异常堆栈。日志文件要按天分割保留至少30天方便追溯历史问题。我给这个系统加了一个简单的监控任务每10分钟扫描一次最近两小时内有过支付回调、但订单状态还是“待支付”的记录自动告警并重推一次查询接口能有效避免掉单率。6.3 数据库字段设计建议对接支付前建议在订单表里预留这些字段不然后面加字段成本只增不减字段名类型说明pay_trade_novarchar(64)易宝交易流水号pay_typevarchar(16)支付方式扫码、快捷、H5等pay_statustinyint0待支付 1已支付 2已退款pay_timedatetime支付成功时间callback_receivedtinyint是否收到过异步回调callback_rawtext最近一次回调原始报文加了callback_received这个字段后我在排查一些历史订单时方便了很多——直接看这个字段就知道是压根没收到回调还是收到了但处理失败。7. 一些容易被忽视的“小”问题7.1 时间同步问题易宝支付接口要求本地时间和服务器时间误差在一定范围内具体数值文档没写死但我实测过如果服务器时间偏差超过2分钟下单请求会直接失败。部署环境如果是云服务器建议开启NTP自动时间同步timedatectl set-ntp true很多云服务器默认时间没问题但一些自建机房的机器时间会漂移这类问题排查起来特别隐蔽——因为代码完全没改但突然就报“请求时间无效”。7.2 证书文件的权限管理退款接口、对账单接口通常要使用商户证书。证书文件不要放到Web可访问目录下比如public/certs/这种也不要提交到Git仓库里。建议放在项目外部目录比如/www/certs/并在部署脚本里通过环境变量引入路径。另外证书文件权限要设为600避免同服务器上的其他用户读取chmod 600 /www/certs/yeepay_merchant.pem7.3 异步通知里的订单号类型问题易宝回调里的orderId字段有的接口文档写的是字符串有的场景下可能是纯数字PHP里用$_POST[orderId]接收后是字符串类型。但你在数据库里可能用int类型存储订单ID直接find($orderId)没问题可一旦你用了强类型比较if ($order-id $orderId) { ... }这里$order-id可能是int$orderId是string会判断为 false。虽然不影响业务正确性但会导致日志里记录“订单校验失败”白白浪费排查时间。统一切换或强制转 int 即可。7.4 支付成功页的防刷新问题用户支付完成后跳转到returnUrl如果在这个页面做下单业务比如发货通知用户刷新页面会重复触发。正确做法是returnUrl页面只做展示实际的业务变更全部放在异步回调里。如果非要在这个页面查订单状态也要确保查询操作没有副作用。8. 我这次对接踩过的最后三个坑8.1 回调日志里字符集乱码导致验签失败某次测试环境回调一直验签失败我打开日志发现回调报文里的商品名称是乱码。一看代码原来项目数据库连接使用的是GBK读取订单商品名时自动转成了GBK而下单请求和回调报文要求的是UTF-8签名串里中文就出问题了。解决方式在支付SDK的调用入口强制设置mb_internal_encoding(UTF-8)并且在下单前对所有业务参数做一次UTF-8转码检查属于老项目改不动全部代码时的保底方案。8.2 使用Laravel的自动事务导致回调幂等失效我最初在回调处理方法上用了DB::transaction包裹订单更新逻辑但没处理好唯一键冲突。易宝第一次回调处理中事务还没提交时第二次回调请求进来查询订单状态仍然是“待支付”于是又插入了一条支付流水导致数据重复。后来我在支付流水表增加了(order_id, trade_no)唯一索引并且在事务开始前强制lockForUpdate彻底解决了并发重复写入问题。8.3 线上环境回调地址被WAF拦截上线后某天发现某个渠道的支付回调一直失败商户后台显示回调重试多次但本地没有收到任何请求日志。我打电话请教了运维同事才定位到是Nginx层加了一款WAF规则把易宝通知请求里的某些关键词误判为SQL注入直接拦截了。处理办法在WAF规则里放行易宝的回调IP段和callback路径或者对回调URL单独设置一条绕过WAF的规则。这个坑出现的频率不高但一旦出现排查方向很容易跑偏所以也给各位提个醒。9. 对接完成之后我还做了这些事接口全部跑通之后我没有急着交差而是补做了两件事。第一件事是写了一份支付接入的交接文档内容包括商户号、密钥存放在哪个配置文件回调地址在哪改日志在哪看如何手动触发订单状态修复以及易宝商户后台的操作指南。这份文档后来成了公司新同事接手支付模块的入门教材也让我自己再回去改代码时省了很多回忆成本。第二件事是做了一个“支付对账小工具”。每天凌晨跑一次把本地已支付订单和易宝后台的账单做比对如果发现两边不一致自动生成差异报表发到运维群。虽然易宝有提供日账单下载接口但拿到账单后还得自己解析、对比这个小工具本质上就是把这些手动工作脚本化。线上跑了两个月发现过一次数据库状态和真实支付状态不一致的漏单就是靠这个小工具捞出来的。停更了很久没写技术博客这次又重新拿起键盘一方面是这套支付对接过程中确实遇到不少有意思的坑另一方面也是想给后来的朋友一个完整的“从零到上线”的参照系。如果你正在做PHP项目支付对接遇到具体报错或者有更好的实现技巧欢迎在评论区聊一聊我看到了会尽量回复。
返回列表