简介这份支付宝免签约即时到帐接口面向使用 ECShop V2.7.3 搭建商城的站长与二次开发人员重点解决未签约支付宝即时到账时的支付接入问题适合需要快速补齐商城支付链路、研究免签约到账通知机制的场景。压缩包共 7 个文件以 6 个 PHP 支付模块与 1 个 TXT 说明文档为主整体约 7KB体量轻便便于部署前查看结构与按需覆盖。包内围绕接口模块、多语言支付语言包和到账通知接收逻辑组织可帮助读者理解 ECShop 支付方式挂载、商户参数配置、异步通知处理之间的对应关系也便于在测试环境中比对缺件、参数传递与回调路径。目前已有 318 人学习或下载适合希望以较小成本验证免签约即时到账方案并对现有 ECShop 支付模块做对照排查的读者参考。1. 免签约即时到账在 ECShop 2.7.3 上到底卡在哪一步老 ECShop 站点最尴尬的一幕后台支付方式里明明装了支付宝前台点“去付款”却卡在签名错误或者付完款订单还挂着“未付款”。这份「支付宝免签约即时到帐接口 for ECShop_V2.7.3」瞄准的就是这个最窄的场景——不折腾商户签约流程用老版即时到账 MD5 网关把ecshop的订单状态字段写入数据库让支付宝接口在 V2.7.3 这套老框架上能直连、能回调、能对账。它适合存量站维护、内网验收环境和教学复现如果新起项目直接上官方开放平台签约版别拿这份包硬撑生产。2. 先把调用链理清插件文件落在哪、谁调谁拿到压缩包先别急着覆盖ECShop 的支付插件是“约定大于配置”的结构文件名、类名、目录位置三样必须对齐错一个就是白屏或者支付方式列表里根本瞧不见它。2.1 接口包的文件位置与后台配置字段绝大多数免签约包解压出来只有三五个文件剩下的靠 ECShop 自带的respond.php兜底。先用find把结构摊平看清楚再决定哪些覆盖、哪些保留# 解压到临时目录别直接往站点根目录扔 unzip alipay_ecshop_2.7.3.zip -d /tmp/alipay_pkg # 没有 tree 就用 find 摊平一眼看完整个包 find /tmp/alipay_pkg -type f | sed s#/tmp/alipay_pkg/## # 典型输出不同打包人略有差异 # includes/modules/payment/alipay.php - 插件主文件位置写死 # languages/zh_cn/payment/alipay.php - 后台配置项的中文标签 # includes/modules/payment/alipay_log.php - 部分包附带的日志类可选 # 安装说明.txt这几个文件各管一摊覆盖前先备份原文件尤其includes/modules/payment/alipay.php——很多站的旧插件里存着已经调通的 partner 和 key。包内文件目标路径作用覆盖前注意alipay.phpincludes/modules/payment/插件类本体负责签名、下单、回调旧文件里的配置会丢先备份alipay.php语言包languages/zh_cn/payment/后台“支付方式”里的名称与描述站点是 GBK 编码要转码alipay_log.phpincludes/modules/payment/可选写回调日志需要logs/目录可写安装说明.txt不用放配置项取值说明只看 partner / key 的来源主文件放好之后进后台“系统设置 → 支付方式 → 安装”才会出现“支付宝即时到账”这一项。这一步装的东西不是文件而是往ecs_payment表插一条记录配置项全部序列化进pay_config字段。配置项典型值说明partner2088 开头 16 位合作身份 ID决定钱进哪个账户key32 位字符串MD5 密钥签名和验签都用它末尾空格是高频事故seller_email收款账号回调时用来比对收款方防止串单servicecreate_direct_pay_by_user即时到账的固定服务名写错直接报错_input_charsetutf-8必须和站点编码一致不然中文商品名乱码transporthttp/https老服务器没开 openssl 时只能走 httppay_config是序列化数组后台每保存一次就整体重写一次。这意味着你在数据库里手改的临时参数管理员点一下“保存”就没了——调试阶段的临时值改完记得同步回后台表单。2.2 一次支付从结算页到网关的完整链路链路不长但每一步出错的表现都不一样先把顺序背下来后面排查能省一半时间用户在flow.php确认订单ECShop 生成order_sn并写ecs_order_infopay_status 0。结算页取出当前支付方式对象调用插件的execute()拿到一段 HTML。execute()内部拼参数、算签名、输出一个带隐藏域、用 JS 自动提交的表单目标是https://mapi.alipay.com/gateway.do。用户在支付页完成付款浏览器被同步跳回return_url一般是respond.php?codealipay。与此同时支付网关服务器向notify_url发 POST同样落到respond.php?codealipay。关键点在第五步respond.php干的事很固定——取code参数、加载includes/modules/payment/{code}.php、实例化类、根据type把请求分发给callback()或notify()。所以插件的文件名必须和code一模一样类名也必须一模一样类名写成alipay_pay而文件名是alipay.phpPHP 会在实例化那行直接报致命错误。同步返回和异步通知走同一个入口但语义完全不同通道触发方可信度该干什么callback()用户浏览器跳回低只做展示查一下状态给个提示notify()支付网关服务器高验签、比对金额、写订单状态我见过不止一个站把发货逻辑写在callback()里用户付款后直接关掉浏览器订单就永远停在未付款——这不是接口的锅是入口用错了。2.3 插件类要补齐的方法与类骨架ECShop 对支付插件类的约定不复杂但__construct和同名方法都要有因为老框架在部分代码路径上还按 PHP4 风格调用?php // includes/modules/payment/alipay.php class alipay { var $code; // 支付方式代码必须等于文件名 var $name; // 后台显示名 var $configure array(); // 后台配置项定义 var $partner; // 合作身份 ID var $key; // MD5 密钥 var $seller_email; // 收款账号 var $gateway https://mapi.alipay.com/gateway.do; function __construct() { $this-alipay(); } // 同名方法负责初始化从 pay_config 里把值读出来 function alipay() { $this-code alipay; $this-name 支付宝即时到账; $cfg $GLOBALS[_CFG][payment] ?? array(); // 由 ECShop 注入 $this-partner trim($this-get_cfg(partner)); $this-key trim($this-get_cfg(key)); // 必须 trim $this-seller_email trim($this-get_cfg(seller_email)); } function get_cfg($name) { /* 从 $this-configure 或全局配置取值 */ } function execute() { /* 返回自动提交的 HTML 表单 */ } function callback($data) { /* 同步返回只展示 */ } function notify($data) { /* 异步通知验签 改订单 */ } }几点血泪经验key一定要trim()从后台复制粘贴过来的密钥尾部经常带一个空格或者换行签名怎么算都不对报错信息却只说“验签失败”$this-code大小写要和文件名严格一致Linux 服务器区分大小写Windows 上跑得好好的搬到线上就 404另外execute()返回的是字符串别在里面直接echo否则表单会跑到页面最顶上用户看到一屏乱码。3. 签名与参数MD5 那 32 位到底怎么算出来的免签约即时到账最容易被说成“玄学”的部分就是签名。其实规则只有三条参数名排字典序、拼成kvkv、末尾接密钥做 MD5。真正让人翻车的是那些规则之外的小细节。3.1 即时到账的必填参数表先明确哪些字段参与签名、哪些只是控制行为。参数少一个不报错但会在支付页提示“参数不合法”多一个不认识的字段会被网关直接忽略看起来一切正常但订单对不上。参数是否必填示例说明service是create_direct_pay_by_user即时到账固定值partner是2088xxxxxxxxxxxx合作身份 IDseller_email是收款账号必须与 partner 对应的收款方一致out_trade_no是20240101123456站内订单号回调时靠它找单subject是订单 20240101123456商品标题中文需转码total_fee是99.00单位元两位小数字符串body否ECShop 订单描述可留空notify_url是https://站点/respond.php?codealipay异步通知地址return_url是同上同步跳回地址payment_type是11 表示商品购买_input_charset是utf-8与站点编码一致sign_type是MD5不参与签名计算两个坑提前说total_fee从数据库order_amount取出来直接拼进去decimal字段可能返回99.0000网关按字符串比较金额多两个零就判失败notify_url带上?codealipay这类查询串是允许的但连接必须公网可达、不能被防火墙拦掉 POST。3.2 排序、拼接、MD5 的三步实现签名函数是整个插件最不该出错的一段写清楚注释后面谁接手都能看懂?php // $params 是待签名的参数数组$key 是 32 位 MD5 密钥 function alipay_sign($params, $key) { // 1) 剔掉 sign / sign_type以及值为空串的参数 unset($params[sign], $params[sign_type]); $filtered array(); foreach ($params as $k $v) { // 注意用 判断不能用 empty()否则 total_fee0 会被丢掉 if ($v || $v null) { continue; } $filtered[$k] $v; } // 2) 按参数名 ASCII 码升序排序 ksort($filtered); // 3) 拼成 kvkv值保持原始状态不做 urlencode $pairs array(); foreach ($filtered as $k $v) { $pairs[] $k . . $v; } $raw implode(, $pairs); // 4) 末尾直接接密钥md5 后转小写大写网关不认 return strtolower(md5($raw . $key)); }逐条解释一下为什么这么写。第一sign和sign_type不参与签名是协议规定第二判空必须用 total_fee在某些配置下算出来是0empty()会把它当空值剔掉签出来的字符串和网关算的不一样报错还看不出原因第三参与签名的必须是原始值URL 编码只发生在拼表单那一步如果先urlencode再签名中文商品名的订单百分之百失败第四ksort排的是键名不是键值而且必须是 ASCII 序PHP 默认的字符串比较就是 ASCII 序一般不会错但用了SORT_NUMERIC之类的参数就全乱了。排查签名问题有个笨办法很有效把签名前的$raw拼串和密钥长度一起写进日志拿网关返回的失败信息对照八成能看出是少参数还是编码不对。3.3 表单自动提交与编码转换签完名就该把用户送出站了。这一步的写法直接决定用户看到的是一个自动跳转还是一个能点的按钮function alipay_form($gateway, $params, $key) { $params[sign] alipay_sign($params, $key); $params[sign_type] MD5; // 网关地址上的 _input_charset 必须和签名里的值一致 $action $gateway . ?_input_charset . $params[_input_charset]; $html form idalipaysubmit namealipaysubmit action . $action . methodPOST; foreach ($params as $k $v) { $html . input typehidden name . $k . value . htmlspecialchars($v, ENT_QUOTES, UTF-8) . /; } $html . /form; $html . scriptdocument.forms[alipaysubmit].submit();/script; return $html; }选择 POST 表单而不是拼一个长 URL 跳转原因很实际老 IE 对 URL 长度有限制商品名带中文时 URL 编码会把长度翻三倍超过 2000 字符直接被截断用户点进去就是参数缺失。POST 没有这个烦恼还能避免被二次转义。编码这一块按站点实际编码处理。站点是 UTF-8 就直接传是 GBK 的话subject和body必须先转码再参与签名转码要在签名之前做签完再转签名就对不上了// 站点编码为 GBK 时的处理顺序先转码再签名 if (strtoupper($_CFG[charset]) ! UTF-8) { $params[subject] iconv(GBK, UTF-8, $params[subject]); $params[body] iconv(GBK, UTF-8, $params[body]); } $params[_input_charset] utf-8; $html alipay_form($this-gateway, $params, $this-key);iconv遇到生僻字会返回空串上线前用后台商品库里最长的那个标题试一次比事后逐个订单排查省事得多。4. 异步通知落库notify 里该校验的四件事与幂等写法回调是整个支付链路里唯一有“钱已经到账”这个事实的入口写错一个字就是重复发货或者丢单。这一段值得多花半天时间写扎实。4.1 验签之外还得比对什么验签只证明“这个请求确实是网关发的”不证明“这笔钱该记在这个订单上”。所以notify()里至少要过四道关第一道验签。用同样的alipay_sign()对$_POST重算一遍和请求里的sign比对注意统一转小写再比。第二道交易状态。只认TRADE_SUCCESS和TRADE_FINISHED两种其它状态比如等待付款直接回success让网关别再推但要写日志留痕。第三道收款方比对。seller_email和partner必须和自己配置的一致防止有人拿别的商户号构造请求。第四道金额比对。把订单表里的order_amount和回调里的total_fee都当字符串比较或者用bccomp($a, $b, 2)直接比浮点数99.00和99在某些情况下会判不相等。顺序也有讲究验签放在最前面不通过就直接返回失败别急着查库否则被刷接口的时候数据库先挨一轮无意义的查询。4.2 用 order_paid() 改状态别手写 UPDATE很多人图省事在notify()里直接UPDATE ecs_order_info SET pay_status 2。这么干的后果是订单操作日志没有记录、用户积分没加、赠品没发、分成没算后台管理员点开订单详情看到的是一片空白的时间线。ECShop 在includes/lib_order.php里提供了order_paid($order_id)该做的事它都做了。调用它之前把订单查出来做完幂等判断再调用function notify($data) { $logfile ROOT_PATH . logs/alipay_notify_ . date(Ymd) . .log; file_put_contents($logfile, date(c) . . json_encode($data) . \n, FILE_APPEND); // 1) 验签 if (empty($data[sign]) || alipay_sign($data, $this-key) ! strtolower($data[sign])) { file_put_contents($logfile, sign_fail\n, FILE_APPEND); exit(fail); } // 2) 交易状态非成功状态直接回 success避免网关反复重推 if (!in_array($data[trade_status], array(TRADE_SUCCESS, TRADE_FINISHED))) { exit(success); } // 3) 收款方比对 if ($data[seller_email] ! $this-seller_email || $data[partner] ! $this-partner) { file_put_contents($logfile, seller_mismatch\n, FILE_APPEND); exit(fail); } // 4) 查订单order_sn 必须转义后再拼 SQL $order_sn addslashes(trim($data[out_trade_no])); $sql SELECT order_id, order_sn, order_amount, pay_status FROM . $GLOBALS[ecs]-table(order_info) . WHERE order_sn $order_sn LIMIT 1; $order $GLOBALS[db]-getRow($sql); if (empty($order)) { file_put_contents($logfile, order_not_found: $order_sn\n, FILE_APPEND); exit(fail); } // 5) 幂等已付款的订单直接回 success不再走后续流程 if ($order[pay_status] PS_PAYED) { exit(success); } // 6) 金额比对用 bccomp 保留两位小数 if (bccomp($order[order_amount], $data[total_fee], 2) ! 0) { file_put_contents($logfile, amount_mismatch: {$order[order_amount]} vs {$data[total_fee]}\n, FILE_APPEND); exit(fail); } // 7) 走 ECShop 官方流程订单日志、积分、赠品都会连带处理 order_paid($order[order_id]); exit(success); }参数说明几个容易忽略的点PS_PAYED是 ECShop 定义的常量值为 2别硬编码数字$GLOBALS[ecs]-table()会自动加表前缀直接写ecs_order_info在改过前缀的站上必然报错$GLOBALS[db]-getRow()是 ECShop 自带的查询封装返回关联数组取不到返回空数组用empty()判断即可。4.3 日志、超时与重复推送网关的推送策略是“没收到 success 就一直重推”间隔从几分钟到几小时不等总共会推若干次。这既是保障也是陷阱如果不做幂等用户会被发好几次货如果notify()处理时间太长比如里面同步调了短信接口网关会判超时继续重推。我的做法是notify()里只做验签、改状态、写日志三件事凡是耗时的动作发短信、推 ERP、生成电子面单全部踢到队列或者另外的定时脚本里。日志按天切分文件名带日期出问题时能按订单号grep出完整的推送轨迹比翻服务器访问日志快得多。还有一点exit(success)输出的字符串必须是纯success前面不能有任何空白、BOM 或者 PHP 报错信息。文件保存成 UTF-8 带 BOM 是重灾区PHP 把 BOM 当正文输出网关收到的就是\xEF\xBB\xBFsuccess判定为失败于是无限重推。5. 常见问题排查五类翻车现场的现象、原因与解法下面这五条基本覆盖了免签约接口在 ECShop 2.7.3 上九成以上的故障按出现频率排序。5.1 一直提示验签失败参数怎么看都没问题现象跳转到支付页立刻返回“签名验证失败”或者“invalid sign”日志里连订单号都没有。原因九成是密钥带的隐形字符。从后台复制key时尾部跟了一个空格、一个换行或者从网页复制时把全角空格带进来了剩下的一成是签名前的参数集合和网关收到的不一致比如某个参数值是null被implode拼成了空串或者用了http_build_query导致值被 URL 编码后才参与签名。解决先var_dump(strlen($this-key))看一眼长度是不是 32不是就trim()加preg_replace(/[\x00-\x1F\x7F]/u, , $key)洗一遍再把签名前的$raw拼串完整打进日志逐字符和网关文档里的示例对照。有个快速自查法拿两个参数手算一遍 MD5和代码输出比对能立刻定位是排序问题还是拼接问题。5.2 用户付了钱订单还是“未付款”现象支付页显示成功回到站内也提示支付成功但后台订单状态纹丝不动。原因callback()里写了“支付成功”的提示但notify()因为验签失败、订单号找不到或者金额对不上而exit(fail)网关重推几次后放弃。也可能是notify_url填的是内网地址或者带端口的测试域名公网根本访问不到。解决先看logs/alipay_notify_YYYYMMDD.log有没有记录。一条都没有说明请求没到服务器检查notify_url是否公网可达、是否有防火墙拦 POST、respond.php是否被别的插件覆盖。有记录但都是sign_fail或amount_mismatch问题就在插件逻辑里按上一节逐条对。5.3 同一笔订单发了好几次货现象用户投诉收到多份发货通知后台订单操作日志里order_paid出现了好几次。原因没有做幂等判断。网关在没收到success时会重推而每次重推都完整走了一遍“改状态 加积分 发赠品”的流程。解决在notify()开头查订单的pay_status等于PS_PAYED就直接exit(success)。这个判断要放在金额比对之前、验签之后既避免重复处理又不给伪造请求可乘之机。另外order_paid()内部对已付款订单也有一定保护但别指望它自己加一层更稳。5.4 商品名带中文就报参数错误现象英文商品名的订单能正常支付中文商品名一律失败。原因站点是 GBK 编码直接把 GBK 的中文塞进参数表并参与签名而_input_charset写的是utf-8两边算出来的字符串字节不同签名自然对不上。解决在签名之前把subject和body用iconv(GBK, UTF-8, $str)转一遍_input_charset固定utf-8如果站点本身是 UTF-8 就不用转但要把_input_charset和站点编码对齐别一边 utf-8 一边 gbk 混着来。5.5 后台改完支付配置前台突然不能付了现象管理员在后台调整了支付方式的排序或者描述保存之后前台支付按钮消失或者跳转时报参数缺失。原因pay_config是序列化字段后台每次保存都会整体重写。如果之前为了调试在数据库里手工塞过参数保存一次就被清空另外某些版本的 ECShop 在保存支付方式时会重建payment表记录code字段大小写被改写和文件名对不上。解决所有配置项老老实实在后台表单里填别直接改库改完支付方式后去includes/modules/payment/确认文件名没被改动测试环境先跑一遍下单流程再放到生产。养成习惯任何涉及pay_config的调整改完先拿一笔一分钱的测试订单走通全链路。6. 上线前的联调自检把回调重放三遍再放手真正让这套接口稳下来的不是代码写得多漂亮而是上线前那半小时的“折腾”。我一般会在测试环境做三件事先用真实参数重放一次notify验证验签和幂等再把同一份 POST 重放三遍确认第二、三次都直接返回success且订单没有任何变化最后手动把total_fee改掉一位小数重放确认金额校验真的能拦住。重放用curl最省事把日志里抓到的真实参数原样贴进去# 用日志里的真实参数重放一次异步通知注意 URL 里要带 codealipay curl -i -X POST https://shop.example.com/respond.php?codealipay \ -H Content-Type: application/x-www-form-urlencoded \ --data partner2088xxxxxxxxxxxxseller_emailpay%40example.comout_trade_no20240101123456total_fee99.00trade_statusTRADE_SUCCESSsign算出来的签名sign_typeMD5 # 期望返回HTTP 200响应体只有 success 四个字母 # 如果返回整页 HTML说明 respond.php 之前有输出检查文件 BOM 和 PHP 短标签上线自检清单我固定跑这几项检查项判定标准不通过的表现插件文件名与类名完全一致且大小写匹配前台支付方式列表空白key长度严格 32 位无空白字符验签必失败notify_url可达性公网 POST 能返回success订单永远未付款幂等同一笔重放三次状态不变重复发货金额比对改一分钱能被拦住少付也能通过日志目录权限logs/可写出问题无从查起那次教训挺深的一个站的notify()里忘了exitrespond.php后面又跟着模板输出返回给网关的是整页 HTML网关判定失败开始无限重推一晚上同一笔订单被处理了十几次。从那以后我每接一个支付插件都强制把notify手动重放三遍、改一次金额、看一眼日志三关全过才敢放到线上。这份包的价值不在于它有多新而在于它把这些老框架上的边界都趟过一遍照着上面的顺序走大部分坑都能提前避开。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站