先把这个标题里的"自动发卡平台"拆清楚,不然容易绕进去。你要接的其实是两样东西:一个是易支付(epay)这套支付网关,负责收钱;另一个是发卡网程序(独角数卡、zfaka 这类开源发卡系统),负责收完钱自动把卡密发出去。所谓"对接",就是让这俩能对上话——用户付了钱,发卡网立刻把卡密吐出来,中间不靠人工。
这件事听起来简单,但发卡网对接和普通商城对接,有一个本质区别:商城卖的是实体或虚拟服务,发卡网卖的是"一串卡密",这串卡密一旦发出去,就再也收不回来了。所以发卡网对接最怕的不是收不到钱,而是"钱收到了、卡密多发了一份"或者"钱收到了、卡密卡在库里发不出去"。这篇就围绕这两个最要命的问题,把完整流程和能直接跑的代码给你。
先想清楚:发卡网对接和普通对接,到底差在哪
普通商城对接易支付,回调里做的是"更新订单状态为已支付",然后发个通知给买家。哪怕回调晚几分钟、重发几次,顶多是用户多刷新几下页面,不会造成实质损失。
发卡网不一样。回调一进来,你要做的是"从卡密库里取一张卡、发给用户、把这张卡标记为已售出"。这三步必须原子化——要么全做完,要么全不做。如果回调重发了一次,你的程序没做幂等判断,就会取出第二张卡发出去,等于一张订单亏两张卡。如果回调来了但取卡失败,订单显示已支付却永远不发卡,用户就会来找你退款。
所以发卡网对接的代码,和商城对接的代码,骨架一样,但内里多了一道"幂等 + 原子锁定"的关卡。这是网上大多数"易支付对接教程"一笔带过、甚至根本不提的地方,也是你上线后第一个会踩的坑。
第一步:在易支付后台拿齐四样东西
对接前,先在易支付后台创建一个支付通道,记下这四样东西,缺一个都跑不通:
- 商户ID(pid):你的商户唯一标识
- 商户密钥(key):签名用的密钥,复制时注意别带换行符
- 网关地址:下单提交的接口地址,一般是
https://你的域名/submit.php - 异步回调地址:发卡网要提供给易支付的通知地址,比如
https://你的发卡域名/pay/notify/epay
这里有个高频坑:商户密钥从控制台复制时,经常末尾粘进去一个看不见的换行符,导致签名永远对不上。你可以在服务器上跑一句 cat key.txt | od -c,看结尾是不是多了个 \n。关于签名报错的根源排查,我之前单独写过一篇,链接放文末相关阅读。
第二步:下单接口,参数怎么拼、签名怎么算
易支付的下单接口是标准的一套参数。核心字段就这几个:
pid:商户IDtype:支付方式,alipay / wxpay / qqpayout_trade_no:你的订单号,必须是唯一的,发卡网里就是订单IDnotify_url:异步回调地址return_url:同步跳转地址(用户付完跳回来的页面)name:商品名称money:金额,单位元,保留两位小数sign:签名sign_type:签名类型,MD5 或 RSA
签名的算法核心一句话:把除 sign 和 sign_type 之外的所有参数,按参数名 ASCII 码从小到大排序,拼成 key=value 用 & 连接,末尾再拼上商户密钥,整体做 MD5。空值参数不参与签名。这段逻辑写错一个字,后台就会一直报"签名错误"。
下面这段是发卡网里下单的 PHP 代码,可以直接抄:
function buildEpayForm($order, $config) {
$params = [
'pid' => $config['pid'],
'type' => $order['pay_type'], // alipay / wxpay
'out_trade_no' => $order['trade_no'], // 发卡网订单号
'notify_url' => $config['notify_url'],
'return_url' => $config['return_url'],
'name' => $order['goods_name'],
'money' => number_format($order['amount'], 2, '.', ''),
];
$params['sign'] = makeSign($params, $config['key']);
$params['sign_type'] = 'MD5';
return $params;
}
function makeSign($params, $key) {
ksort($params); // 按参数名 ASCII 升序
$str = '';
foreach ($params as $k => $v) {
if ($v === '' || $v === null) continue; // 空值不参与
$str .= $k . '=' . $v . '&';
}
$str = rtrim($str, '&');
$str .= $key; // 末尾拼商户密钥
return md5($str);
}
拿到 $params 后,发卡网通常用 POST 表单跳转到网关地址 submit.php,用户就进了支付页。这一步和普通商城完全一样,没难度。
第三步:回调验签,先验签再发货(顺序绝对不能反)
用户付完钱,易支付会往你的 notify_url 发一个异步通知,带上一堆参数,其中最重要的是 trade_status(必须是 TRADE_SUCCESS)和 sign。
回调处理有一个铁律:先验签,验签通过再发货,发货成功才回 success。顺序反了,就等着被人伪造回调白嫖卡密。验签算法和下单一样,把收到的参数(去掉 sign 和 sign_type)排序拼接,再拼密钥做 MD5,跟传来的 sign 比对。
下面是回调处理的完整代码,重点看幂等和原子锁定这两块:
function handleNotify($post, $config) {
// 1. 先验签,不通过直接拒绝
$sign = $post['sign'];
unset($post['sign'], $post['sign_type']);
if (makeSign($post, $config['key']) !== $sign) {
return 'fail'; // 验签失败,不发货
}
// 2. 确认交易成功
if ($post['trade_status'] !== 'TRADE_SUCCESS') {
return 'success'; // 非成功状态,先应下,别发货
}
$tradeNo = $post['out_trade_no'];
$money = $post['money'];
// 3. 幂等:这笔订单如果已经发过卡,直接返回 success,绝不重复发
if (orderAlreadyDelivered($tradeNo)) {
return 'success';
}
// 4. 金额校验:回调金额必须和订单金额一致
if (!amountMatches($tradeNo, $money)) {
return 'fail';
}
// 5. 原子锁定:加锁防止并发回调同时取卡
if (!tryLockOrder($tradeNo)) {
// 没抢到锁,说明另一个回调正在处理,稍后易支付会重发
return 'success';
}
try {
// 6. 扣卡、发卡、标记已售出,三步必须在一个事务里
$card = getOneCard($tradeNo); // 取一张未售出的卡
if (!$card) {
throw new Exception('no card'); // 没卡了,抛异常,回滚
}
deliverCard($tradeNo, $card); // 发给用户
markCardSold($card['id'], $tradeNo);// 标记已售
markOrderPaid($tradeNo); // 订单置为已支付
commit();
return 'success';
} catch (Exception $e) {
rollback();
return 'fail'; // 处理失败返回 fail,易支付会重发通知
} finally {
releaseLock($tradeNo);
}
}
这段代码里有三个发卡网特有的点,值得单独拆开说。
第一,幂等判断要放在取卡之前。易支付的通知在没收到 success 时会重发,可能间隔几秒重发好几次。你的程序必须记住"这个订单号已经发过卡了",第二次、第三次回调进来直接返回 success,不能再去取第二张卡。判断依据就是订单号,存一张"已发货订单表"或者给订单加个状态字段。
第二,取卡、发货、标记售出要包在同一个事务里。如果取卡成功、发货成功,但标记售出那一步因为数据库抖动失败了,这张卡就会被当成"没卖出去",下一个人又能买到同一张卡密。事务保证这三步要么全成、要么全回滚。
第三,没卡了要返回 fail 而不是 success。如果返回 success,易支付就认为通知成功、不再重发,可你这边其实没发卡,订单就永远卡在"已支付未发货"状态。返回 fail,易支付会按策略重发,等你的卡库补货了还能再补发。
第四步:同步回跳页面,别让它背发货的锅
很多新手会把发货逻辑写在同步回跳(return_url)里,这是发卡网对接最常见的错误之一。
同步回跳是"用户付完钱浏览器跳回来的那个页面",它有两个致命问题:一是用户可能付完钱直接关掉浏览器,根本不会跳回来;二是同步回跳可以被用户手动刷新、重复触发。所以发货只能靠异步回调,同步回跳只负责"展示结果"——订单如果已经发货,就显示卡密;如果还没发货,就显示"支付成功,卡密稍后发放"。
同步页面的逻辑一句话:查一下订单状态,已发货就展示卡密,没发货就提示等待,仅此而已,不要在这里写任何扣卡、发卡的动作。
第五步:上线前,用一分钱把整条链路炸一遍
代码写完了,别急着上线。发卡网对接上线前,我建议你用一分钱(或者测试商品)把整条链路完整跑一遍,重点验证这几件事:
- 付完钱,卡密是不是在几秒内自动发出来
- 同一笔订单,手动模拟回调重发三次,看会不会发出三张卡
- 把卡库里的卡全部标记为已售,再下一单,看回调会不会返回
fail并持续重试 - 故意改错一个签名,看回调是不是被拒、不发货
这四条里,第二条和第三条是最容易出问题的。第二条测的是幂等,第三条测的是"没卡时的失败处理"。很多发卡网上线后半夜被薅,就是因为幂等没做好,回调重发一次就多发一张卡。
几个发卡网特有的坑,提前给你排了
第一个坑:订单号和易支付的 out_trade_no 必须一一对应且唯一。发卡网的订单号如果用了自增 ID,记得别和别的支付方式(比如手动转账)的订单号混在同一个命名空间里,否则回调来了对不上号,发错卡。
第二个坑:金额要用字符串比较,别用浮点数。回调里的 money 是字符串,比如 9.90,你如果直接转成 float 跟订单金额比,浮点精度会出问题,导致一笔 9.9 元的订单永远校不过去。统一转成"分"(整数)再比,最稳妥。
第三个坑:发卡网一般在宝塔或虚拟主机上跑,记得把回调地址加到防火墙白名单。有些站长开了 WAF 或防盗链,把易支付服务器的回调请求当成攻击拦了,结果订单一直显示"未支付"。如果你遇到回调收不到,别急着怀疑签名,先看服务器日志里有没有进来请求。
如果你用的是已经在跑的旧发卡系统,想从 V1 协议升级到 V2,那套改造逻辑和这里讲的从零对接又不一样,签名体系、公共参数、回调规范全变了,建议单独看这篇发卡网接入epay V1与V2协议差异:适配改造与兼容逻辑拆解。至于下单和回调那套签名算法的完整参数表和排错细节,我在易支付 API 接口对接全流程:参数表 + 签名算法 + 一段能直接跑的代码里拆得很细,卡在验签上可以去翻。
说到底,发卡网对接易支付,难的不是调通接口,而是把"自动发卡"这件事做安全——钱没到手是小事,卡密多发、发错、发了收不回,才是真金白银的损失。把幂等、原子锁定、回调失败处理这三件事做扎实,比纠结用 MD5 还是 RSA 重要得多。