易支付 API 对接,网上教程一抓一大把,但几乎全是"单商户、只讲下单、贴一段能跑的代码"就完了。真正让对接从"能跑"变成"能上线、能商用、能同时服务一堆商户"的三件事,几乎没人讲透:V1 和 V2 到底差在哪、怎么选;测试环境怎么搭才不会污染生产;多商户怎么隔离才不会串数据。
这三件事,恰恰是商业对接里 90% 的坑所在。这篇文章就只讲这三块,不扯别的。下单参数和签名算法那种基础的东西,我之前在易支付 API 接口对接全流程里已经拆得很细了,这里不再重复,直接往上走一层。
先搞清楚一件事:V1 和 V2 不是"升级",是两套协议
很多人一听 V2 就以为是 V1 的"新版本",升级一下就行。这是对接易支付最大的认知误区。V1 和 V2 是两套并存的协议,不是新旧迭代关系,你要么用 V1,要么用 V2,不存在"从 V1 平滑升到 V2"这回事。
两者的核心差异,我用一张表说清楚,这是你在决定用哪套之前必须看清的:
| 维度 | V1 协议 | V2 协议 |
|---|---|---|
| 接口入口 | submit.php / mapi.php,分散独立 | 统一 /api/pay/ 前缀,REST 风格 |
| 签名算法 | MD5 对称签名,一把密钥 | RSA 非对称签名,公私钥分离 |
| 提交格式 | 表单 form-urlencoded 为主 | 部分要求 JSON 报文 |
| 防重放 | 无(缺时间戳) | 新增 timestamp 时间戳参数 |
| 退款接口 | 无统一标准,服务商自定义 | 标准化 /api/pay/refund |
| 上手难度 | 低,几行代码能跑通 | 高,要生成 RSA 密钥、懂非对称签名 |
这里有个很多人忽略的决策逻辑:不是越新越好,是看你的场景和上游服务商支持什么。
如果你的场景是个人小工具、发卡站、临时收个款,V1 够用,MD5 签名简单,密钥就一把,出问题好排查。但 V1 有个致命弱点:密钥是对称的,密钥一旦泄露,攻击者既能伪造你的下单,也能伪造回调骗你发货。所以 V1 只适合低风险、低金额的场景。
如果你的场景是正经商业系统、要接多个下游、要退款、要防重放攻击,那 V2 是唯一选择。RSA 非对称签名把"签名用的私钥"和"验签用的公钥"分开了,商户手里只有私钥(或者只有公钥,看服务商设计),就算某一端泄露,攻击者也没法同时伪造请求和回调。
至于 V1/V2 更底层的签名细节、RSA 密钥怎么生成,站里那篇V1 与 V2 协议差异拆解从发卡网适配的角度写得很细,你要做协议切换可以对照着看,这里不展开重复。
测试环境:大多数人对接完,从来没真正"测"过
这是全文我最想讲的一块,因为网上几乎没有一篇易支付对接教程,认真讲过"测试环境"这回事。大家都默认你照着文档写完代码,本地跑一遍返回 code:1 就完事了。但你有没有想过:你本地跑通,和你对接的到底是"测试环境"还是"生产环境"?你那一笔 0.01 元的测试单,是真实扣款了吗?你验证过回调真的能触发吗?
对接易支付的正确姿势,是先把整条链路在测试环境里走通,再切生产。而测试环境,不是"换一个域名"那么简单。
测试商户号和测试密钥要分开
正规的易支付服务商,会给你两套商户凭证:一套测试的(测试 pid + 测试密钥),一套生产的(生产 pid + 生产密钥)。这两套 pid 和密钥是不同的,千万不能混用。
为什么要分开?因为测试环境的订单、回调、金额都是"假的",它不会真实扣款,也不会真实入账。如果你拿生产的 pid 去测试,那你每一笔测试单都是真实交易,钱真的会从测试账户(或者你自己)扣走;如果你拿测试的 pid 上线,那用户付的钱进了测试环境,生产订单永远收不到钱。
所以第一步,先找服务商确认有没有测试商户号,把它和生产商户号严格隔离在配置里区分开。代码里用一个环境变量(比如 EPAY_ENV=dev/prod)切换,别在代码里硬编码 pid。
测试金额:1 分钱,但要确认它到底扣没扣
测试环境里,一般用 0.01 元(1 分钱)来验证支付链路。但这里有个容易踩的坑:有些服务商的"测试环境"其实不是真沙箱,它只是给你一个测试 pid,背后走的还是真实通道。也就是说,你以为在测试,其实每一笔 1 分钱都是真实从你银行卡/余额里扣的。
所以测试前一定要问清楚服务商:这个测试 pid 走的是沙箱(不真实扣款),还是真实通道(真实扣款)?如果是真实通道,那你的"测试"就要控制次数,别一口气刷几十笔,最后发现自己被扣了几块钱还以为是系统 bug。
另外,真实通道的 1 分钱支付,往往有最低金额限制或者会被风控拦截。如果 1 分钱付不出去,别急着怀疑代码,先试试 0.1 元或者 1 元,大概率是通道对极小金额做了限制。
测试回调:本地怎么收得到异步通知
这是测试环境里最卡人的一环。易支付的下单、查单是你主动请求,本地能跑;但异步回调是易支付服务器主动来访问你的 notify_url,你本地电脑在内网,易支付的公网服务器根本访问不到你的 localhost。
所以本地联调回调,必须解决"让你的本地服务暴露到公网"这个问题。常用的办法是内网穿透(比如 frp、ngrok、花生壳之类),把一个公网临时地址映射到你本地的回调接口,然后把 notify_url 填成这个穿透地址。
但这里有两个高频坑,网上几乎没人提醒:
第一,内网穿透地址是临时的、会变的。你每次重启穿透工具,地址可能就换了。如果你把穿透地址写死在代码或配置里,下次重启后回调就断了。所以穿透地址要放配置文件,别硬编码。
第二,测试环境的回调地址,上线前必须换成生产地址。这个错误极其隐蔽:你本地用穿透地址测通了回调,代码里留着 notify_url = 穿透地址,上线时忘了改,结果生产环境里用户付完钱,回调全发到了那个已经失效的穿透地址上,订单状态永远不更新,用户付了钱却显示"未支付"。
一个实操建议:把 notify_url 和 return_url 也做成环境变量,和 pid、密钥一起,跟着 EPAY_ENV 切换。切生产环境时,四个参数一起变,就不会漏。
怎么验证回调逻辑真的对
很多人测回调,就是发起一笔支付,然后看回调有没有进来。但这只测了"回调能通",没测"回调逻辑对不对"。
回调逻辑要重点验证三件事:
第一,验签。回调进来后,你要用密钥重新算一遍签名,和回调里的 sign 比对,对不上就拒绝。很多人的回调接口是"裸奔"的,收到就改订单状态,这等于把"改订单状态"这个最敏感的操作,开放给了任何能访问你回调地址的人。
第二,金额校验。回调里的 money 要和你的订单金额一致,不一致就是异常,不能随便发货。这是防"改金额"的关键,很多人漏了。
第三,幂等。易支付回调会重发(最多重试几次),你的回调处理必须幂等——同一笔订单收到重复回调,不能重复发货、重复入账。用订单号做唯一约束,处理前先查是否已处理过。
这三件事,才是回调测试真正的重点。光看"回调进来了"远远不够。
多商户隔离:一个平台服务一百个商户,数据不能串
这是第二个网上几乎不讲透的点。易支付对接,单商户很简单,但一旦你是个 SaaS 平台、或者中间商,要同时服务几十上百个下游商户,问题就来了:每个商户有自己的 pid 和密钥,数据(订单、流水、余额)都要严格隔离,商户 A 绝对不能看到、更不能操作商户 B 的任何数据。
文章开头那个"看到别人订单"的事故,本质就是多商户隔离没做好。下面我把多商户隔离的四个关键点拆开讲。
第一层:pid 和密钥,必须一商户一套
这是隔离的基础。每个下游商户,都要有自己独立的 pid 和独立的密钥。绝不允许多个商户共用同一个 pid——共用 pid 意味着所有订单都混在同一个商户号下,你根本分不清这笔单是谁的,对账、分账、退款全乱套。
密钥更是如此。每个商户的密钥要独立生成、独立存储、独立管理。密钥存储要加密(不能明文落库),并且要做成"每个商户只能拿到自己的密钥"的权限模型,不能一个管理员接口把所有商户的密钥一次性全吐出来。
第二层:数据查询,必须强制带上商户维度
这是最容易出 bug 的地方。你的查询接口(查订单、查流水、查余额),SQL 里必须强制带上 merchant_id 条件,而且这个 merchant_id 必须来自"当前登录商户的身份",不能来自请求参数。
文章开头那个事故,就是反例:查询接口的 pid 是从请求参数里读的,前端传什么 pid 就查什么 pid 的数据,结果商户 A 把 pid 改成商户 B 的,就能看到商户 B 的订单。
正确的做法是:商户身份从登录态(session/token)里取,后端把这个身份映射成 merchant_id,再拼进 SQL。请求参数里不允许传"要查谁的 pid",这样商户无论怎么改参数,都只能查到自己。
这个原则叫"服务端强制隔离",是所有多租户系统的铁律。数据隔离的底层表结构怎么设计(商户表、订单表怎么建),我在聚合支付数据库设计那篇里详细列过字段清单,这里不再重复,但逻辑上是一致的:订单表必须有 merchant_id 字段,所有查询都走它。
第三层:回调路由,怎么知道这笔回调是哪个商户的
多商户场景下,回调是个容易忽略的隔离点。每个商户在下单时,会传自己的 notify_url,易支付回调时,会把这个商户的单子回调到"这个商户下单时填的那个地址"。
这里有两种架构,隔离方式完全不同:
第一种,商户各自填自己的回调地址。易支付直接回调到商户自己的服务器,你的平台不参与回调。这种最简单,隔离天然成立,因为回调地址是商户自己填的、指向商户自己的服务器。缺点是商户要自己处理验签、自己维护回调服务。
第二种,平台统一回调地址,由平台转发。所有商户下单时 notify_url 都填平台的一个统一地址,平台收到回调后,根据回调里的 pid(或 out_trade_no)识别这是哪个商户的单子,再转发给对应商户。这种模式下,平台承担了"回调路由"的职责,好处是商户不用管验签,坏处是平台回调路由一旦写错,就会把商户 A 的回调转发给商户 B,这是新的串数据风险点。
如果你走第二种(平台转发),回调路由的匹配逻辑必须用 out_trade_no 或 pid 精确匹配到唯一商户,匹配不到就丢弃并告警,绝不能"匹配不到就发给默认商户"这种兜底逻辑。
第四层:密钥泄露的隔离边界
多商户隔离的终极拷问是:如果商户 A 的密钥泄露了,攻击者能干什么?能影响到商户 B 吗?
答案是:只要隔离做对了,攻击者拿到商户 A 的密钥,最多只能伪造商户 A 自己的下单和回调,碰不到商户 B 的任何数据。因为商户 A 的密钥只能签商户 A 的 pid,签名校验时 pid 和密钥是绑定的,攻击者没法用 A 的密钥去操作 B 的订单。
但这里有个前提:你的密钥校验逻辑必须是"用商户自己的密钥去验商户自己的单",而不是"用一个全局密钥验所有商户"。如果你图省事,搞了个全局密钥,那一个商户泄露,全场沦陷,隔离就白做了。
所以多商户隔离的最后一道防线,就是密钥和商户强绑定、一一对应。这也是为什么 V2 的 RSA 非对称签名在多商户场景下更安全——每个商户一套公私钥,天然隔离,比 V1 的一把对称密钥更不容易出现"全局密钥"这种偷懒设计。
把三件事串起来:一个能上线的对接清单
最后,我把 V1/V2 选择、测试环境、多商户隔离这三件事,串成一张上线前的检查清单。你对接易支付,上线前逐条过一遍:
- 协议选择:低风险小工具用 V1,正经商用/多商户/要退款用 V2,别混用。
- 环境隔离:pid、密钥、notify_url、return_url 四个参数全部走环境变量,dev 和 prod 两套,切换时一起切。
- 测试商户号:确认测试 pid 走的是沙箱还是真实通道,别拿生产 pid 当测试用。
- 回调验证:验签 + 金额校验 + 幂等,三件事都测过才算回调测通,光看回调进来不算。
- 多商户 pid:每个商户独立 pid + 独立密钥,绝不共用。
- 查询隔离:merchant_id 从登录态取,不从请求参数取,SQL 强制带商户维度。
- 回调路由:平台转发模式要用 out_trade_no/pid 精确匹配,匹配不到就丢弃告警。
- 密钥绑定:密钥和商户一一对应,杜绝全局密钥。
把这八条捋顺了,你的易支付对接才算是从"本地能跑"迈到了"能上线、能商用、能同时服务多个商户"。对接这件事,写代码只占三成,剩下七成全是这些环境隔离和数据隔离的细节——而这些细节,恰恰是网上那些"贴一段代码"的教程,永远不会告诉你的。