
简介一套在线虚拟商品自动交易发卡平台源码面向个人站长与虚拟商品卖家基于ThinkPHP5与Layui2.2开发支持支付宝、微信免签接口及第三方个人支付可自动完成虚拟商品交易与发货适合搭建发卡网、自动发货系统等场景。包内共2000个文件以1137个PHP业务代码、136个PHPT模板、58个JS脚本、58个HTML页面和57个Markdown文档为主同时包含CSS样式、图片素材、数据库SQL与配置文件压缩包整体约13.39MB。已有653人学习下载。源码全开源无域名与安装次数限制附带安装教程后台默认账号admin、密码123456导入数据库并修改database.php即可快速部署。目录按ThinkPHP模块化组织前端基于Layui支付与自动发货逻辑完整适合PHP开发者二次开发或直接上线运营。对于需要个人免签支付的虚拟商品商家这份源码提供了完整的支付与发货闭环值得收藏参考。1. 免签接口源码与第三方个人支付发卡平台自动交易的低门槛路径做在线虚拟商品自动交易发卡平台最绕不开的一环不是商品管理不是卡密存储而是支付。个人开发者去申请支付宝或微信的官方支付接口往往卡在营业执照、企业资质和签约审核上。于是“免签接口”成了这个领域最常见的替代方案平台在用户下单后生成收款二维码用户扫码付款平台通过某种方式收到支付成功的通知再自动把卡密发货给用户。整个链路完全无人值守。这篇博文要讲的就是“第三方个人支付”这类免签接口的完整落地路径从回调验签、订单状态机到自动发货和防掉单都在覆盖范围内还会给出可直接改写的源码实现。适合正在自建发卡系统、想了解免签接口工作原理或者被回调通知、掉单问题折磨过的开发者阅读。2. 免签接口的两条实现路线与选型依据2.1 路线一使用第三方聚合支付平台最常见的做法是接入市面上已有的“第三方个人支付”聚合平台。这类平台本身已经解决了个人收款和异步通知的技术难题对外提供统一下单、订单查询、回调通知等接口。发卡平台只需要按照其文档接入就能获得支付宝、微信的扫码支付能力。// 第三方聚合支付统一下单示例伪代码参数名以实际平台文档为准 $params [ merchant_id 你的商户号, order_id $orderNo, // 平台内部订单号 amount $productPrice, // 金额单位元 notify_url https://your-domain.com/api/pay/notify, return_url https://your-domain.com/order/result, sign md5($merchantId . $orderNo . $productPrice . $apiKey) ]; $response httpPost(https://pay.example.com/api/create, $params); // 返回中通常包含 payment_url 或 qr_code前端展示二维码即可这里sign的生成规则各平台大同小异常见做法是把除签名外的参数按 ASCII 码排序后拼接加上商户密钥做 MD5。下单成功后平台返回一个二维码地址或跳转链接用户在手机上完成扫码付款。2.2 路线二自建监听方案如果不想依赖第三方平台可以选择自建免签方案自己开发一个接收支付结果的后端服务监听异步通知。这条路的工作量集中在两个地方一是接收支付宝/微信官方服务器发来的回调通知二是把通知与订单关联并触发发卡流程。# Flask 接收异步通知示例仅演示通知接收与验签框架 from flask import Flask, request import hashlib app Flask(__name__) APP_KEY 你的密钥 app.route(/api/pay/notify, methods[POST]) def pay_notify(): data request.form.to_dict() sign data.pop(sign, ) raw .join(f{k}{v} for k, v in sorted(data.items())) if hashlib.md5((raw APP_KEY).encode()).hexdigest() ! sign: return fail # 验签失败通知方会重试 order_no data.get(out_trade_no) # 更新订单状态 触发发货见第 3 章 return success # 必须返回 success否则支付方会反复通知自建方案的优势是没有中间平台抽成缺点是所有支付渠道都要自己对接。对于发卡平台来说绝大多数开发者会选择路线一因为第三方个人支付平台已经把“个人收款码收款”与“程序化通知”之间的桥搭好了。2.3 怎么选看单量和回调稳定性选择的关键指标只有一个回调成功率。第三方个人支付平台的回调链路本身就是它最大的产品壁垒稳定的平台能做到 99% 以上的回调到达率。自建方案的典型问题是个人收款码无法程序化读取付款方信息只能靠用户输入单号或金额尾数来匹配订单这个体验会让自动交易大打折扣。初创阶段的发卡平台我一般建议直接接第三方个人支付把精力放在卡密管理和自动发货上支付这块交出去。单量上来后再评估自建方案或者直接在第三方平台上走“商家代付”类产品本质上是把免签接口升级成合规签约接口。3. 发卡平台自动交易从下单到自动发货的完整源码实现3.1 订单表与商品表设计自动交易发卡平台的核心数据模型比普通电商简单商品需要预先导入卡密库存订单需要记录支付状态和发货状态。下面是常用的两张表结构-- 商品与卡密表 CREATE TABLE product_card ( id int(11) NOT NULL AUTO_INCREMENT, product_id int(11) NOT NULL COMMENT 商品ID, card_content text NOT NULL COMMENT 卡密内容可多行, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0未售 1锁定 2已售, sold_at datetime DEFAULT NULL COMMENT 售出时间, PRIMARY KEY (id), KEY idx_product_status (product_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 平台订单号, product_id int(11) NOT NULL, amount decimal(10,2) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已关闭, card_id int(11) DEFAULT NULL COMMENT 发货的卡密ID, pay_time datetime DEFAULT NULL, create_time datetime DEFAULT NOW(), PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单状态流转是自动交易的关键用户在页面上下单时系统先锁定一张卡密status1同时生成一个待支付订单。回调到达且验签通过后把订单置为已支付卡密置为已售返回卡密内容给用户。这样设计的核心目的是防止超卖——下单瞬间锁定库存而不是等到支付成功再扣库存。3.2 回调验签与订单状态更新无论接哪家第三方个人支付回调处理代码的结构都差不多。下面是 PHP 版本的完整处理逻辑public function handleNotify(Request $request) { $data $request-all(); $sign $data[sign] ?? ; unset($data[sign]); // 1. 验签按 key 排序拼接 密钥 MD5 ksort($data); $str urldecode(http_build_query($data)) . key . $this-apiKey; if (md5($str) ! strtolower($sign)) { return fail; // 验签失败让支付平台重试 } // 2. 校验订单存在 $orderNo $data[order_id]; $order Db::table(orders)-where(order_no, $orderNo)-first(); if (!$order) { return success; // 订单不存在不再重试人工排查 } // 3. 幂等判断已经是已支付/已发货直接返回成功 if ($order[status] 1) { return success; } // 4. 金额校验防止支付金额与订单金额不一致 if (abs(floatval($data[amount]) - floatval($order[amount])) 0.01) { return fail; } // 5. 更新订单 发货事务 Db::transaction(function () use ($orderNo) { Db::table(orders)-where(order_no, $orderNo) -update([status 2, pay_time date(Y-m-d H:i:s)]); $cardId Db::table(orders)-where(order_no, $orderNo)-value(card_id); Db::table(product_card)-where(id, $cardId) -update([status 2, sold_at date(Y-m-d H:i:s)]); }); // 6. 触发用户通知短信、邮件或站内信异步执行 Queue::push(new SendCardJob($orderNo)); return success; }这段代码有四个关键点。验签失败必须返回非success内容让支付平台继续重试这是回调机制的基本约定。幂等判断必须放在金额校验之前因为重复通知到达时订单已经发货此时返回success可以终止通知循环。金额校验不能省否则用户支付了错误金额也拿到了卡密。发货操作和订单更新必须在同一个事务里避免出现订单已支付但卡密没发出去的情况。这个方案的选型理由很直接把回调处理做成同步事务确保数据一致性优先用户通知放在队列里异步执行快速响应回调。对于发卡平台这个体量Redis 队列或 MySQL 表驱动队列都足够了。3.3 自动发货中的并发与性能边界自动交易平台的另一个隐患是并发回调导致卡密多发或重复发。上面的代码用事务解决了订单状态与卡密状态的原子性但无法解决“同一笔订单回调两次”与“同一张卡密被多笔订单锁定”的并发问题。解决卡密锁定主要是下单环节的问题。常见的做法是使用条件更新代替先查后改// 下单时锁定卡密只更新第一条未售的卡密 $locked Db::table(product_card) -where(product_id, $productId) -where(status, 0) -limit(1) -update([status 1]); if (!$locked) { return 库存不足; }MySQL 的UPDATE语句会锁定匹配的行两个并发请求同时执行时第二个会等待第一个提交或回滚后才继续执行。这样可以天然避免两张订单锁定同一张卡密。需要明确的是limit(1)在UPDATE中不是标准 SQL 语法但 MySQL 和大多数国内云数据库都支持这也是发卡平台最常见的实现方式。回调的高并发场景可以用 Go 写一个轻量级的消费服务用 channel 做并发控制// Go 并发消费回调通知示例 var sem make(chan struct{}, 10) // 最多 10 个并发处理回调 func handleNotify(w http.ResponseWriter, r *http.Request) { sem - struct{}{} // 获取信号量 defer func() { -sem }() // 释放信号量 // 验签、幂等判断、更新订单发货 processOrder(r) w.Write([]byte(success)) }sem这个带缓冲的 channel 限制了同时处理的回调数量避免瞬时回调洪峰打满数据库连接。发卡平台的订单峰值通常远低于这个量级但加上这个控制成本极低能防止支付平台批量补发回调时把接口拖垮。3.4 掉单补偿机制回调通知虽然稳定但微信和支付宝都有通知超时重发的机制重发间隔一般是 15 秒、30 秒、60 秒最多 24 小时。如果在这期间发卡平台服务刚好重启或接口报错回调就会丢失用户的订单会一直停留在待支付状态。解决办法是主动查询对账。第三方个人支付平台一般提供订单查询接口发卡平台需要定时扫描超时未支付的订单主动向支付平台确认支付状态// 每分钟执行一次的补单脚本 $pendingOrders Db::table(orders) -where(status, 0) -where(create_time, , date(Y-m-d H:i:s, time() - 120)) -limit(50) -get(); foreach ($pendingOrders as $order) { $payStatus $this-queryPayStatus($order[order_no]); if ($payStatus paid) { $this-processPaidOrder($order); // 走和回调一样的发货逻辑 } }补单脚本的核心是复用回调那边的处理逻辑不要单独写一套发货代码否则很容易出现状态判断不一致。上面processPaidOrder内部直接调用与回调相同的发货方法。4. 免签接口源码的部署链路与订单状态机优化4.1 从提交订单到发货的完整链路图发卡平台的自动交易链路可以拆成六个环节用户在商城页面点击购买、请求后端生成订单并锁定卡密、弹出二维码或跳转收银台、第三方支付平台回调通知、后端验签并更新状态、系统推送卡密信息给用户。这里有一个容易被忽视的细节很多入行不久的开发者会把“自动发货”和“展示卡密”混为一谈。用户支付成功后系统把卡密展示在订单详情页这意味着订单接口必须同时校验用户身份和订单归属确保只有购买者本人能看到卡密内容。最常见的实现是生成一个带签名的查询地址// 生成卡密查询链接带有一次性 token $token md5($orderNo . $cardId . $secretKey . $order[create_time]); $queryUrl https://your-domain.com/order/card?order_no{$orderNo}token{$token};token与create_time绑定即使 URL 泄露也无法长期有效。这不是必要的复杂度但虚拟商品交易里卡密被爬虫抓取、被分享到公开群组的案例实在太多了加一层签名保护成本极低。4.2 订单状态机避免“已支付但未发货”的边界状态订单状态从 0 到 3四个状态的流转里藏着两处边界。第一处是下单锁定卡密后用户始终未支付卡密一直被锁定直到订单关闭第二处是支付回调到达但发货事务失败订单状态停在 1卡密状态仍在 1。第二处边界最致命因为用户付了钱却拿不到东西。解决思路是把“标记支付”和“实际发货”拆成两个步骤发货失败时允许重试// 重试机制订单已支付但尚未发货的由定时任务统一补偿 $paidUnshipped Db::table(orders) -where(status, 1) // 已支付未发货 -where(updated_at, , date(Y-m-d H:i:s, time() - 60)) -limit(100) -get(); foreach ($paidUnshipped as $order) { try { Db::transaction(function () use ($order) { Db::table(orders)-where(id, $order[id]) -update([status 2]); Db::table(product_card)-where(id, $order[card_id]) -update([status 2, sold_at date(Y-m-d H:i:s)]); }); } catch (\Exception $e) { // 记录日志等待下轮重试 Log::error(发货失败: . $e-getMessage(), [order $order[order_no]]); } }这个定时任务和 3.4 的补单脚本不同补单解决的是“支付了但平台不知道”的问题这里解决的是“平台知道了但发货动作失败”的问题。两个任务并存才能把状态机拆干净。4.3 服务部署PHP-FPM 与队列进程的配合发卡平台通常部署在一台低配云服务器上Nginx PHP-FPM MySQL 是主流组合。回调接口作为 HTTP 接口运行在 PHP-FPM 里异步消耗时间较长的任务如发送邮件、推送卡密到用户微信要放到队列进程里避免回调接口耗时过长导致支付平台超时重试。队列组件我一般用 Redis PHP Resque 或者直接用 Laravel 的 Queue。下面是一个极简的 MySQL 队列实现CREATE TABLE jobs ( id int(11) NOT NULL AUTO_INCREMENT, queue varchar(50) NOT NULL DEFAULT default, payload text NOT NULL, attempts tinyint(4) NOT NULL DEFAULT 0, reserved_at datetime DEFAULT NULL, available_at datetime NOT NULL, PRIMARY KEY (id), KEY idx_queue_available (queue, available_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;// 消费脚本命令行运行supervisor 守护 while (true) { $job Db::table(jobs) -where(queue, default) -where(available_at, , date(Y-m-d H:i:s)) -whereNull(reserved_at) -orderBy(id) -first(); if (!$job) { sleep(2); continue; } Db::table(jobs)-where(id, $job[id]) -update([reserved_at date(Y-m-d H:i:s)]); try { $handler unserialize($job[payload]); $handler-handle(); Db::table(jobs)-where(id, $job[id])-delete(); } catch (\Exception $e) { Db::table(jobs)-where(id, $job[id]) -increment(attempts); Log::error(队列任务执行失败, [job_id $job[id], error $e-getMessage()]); } }available_at字段用于延迟任务比如发货后 10 分钟自动确认、48 小时未支付自动关闭订单都可以通过它做轻量级定时调度不需要额外引入 cron 脚本。发卡平台的队列积压不会很严重MySQL 表队列的吞吐完全够用需要提高并发时再考虑 Redis。5. 免签接口落地后的 3 个验证方法与避坑技巧5.1 验证回调验签逻辑模拟支付平台发送通知免签接口最难调试的就是回调验签。支付平台不会因为你在调试就给你反复发送通知本地把通知模型写好、签名算法写对再用脚本模拟通知请求是最好的验证方式# 模拟回调通知验证验签与发货逻辑 curl -X POST https://your-domain.com/api/pay/notify \ -d order_idTEST20250101001amount19.90pay_typealipaysign$(php -r \$data [order_idTEST20250101001,amount19.90,pay_typealipay]; ksort(\$data); echo md5(urldecode(http_build_query(\$data)).keyyour_api_key); )这个命令把签名生成和请求发送放在一起完成能快速验证三个问题签名算法是否和文档一致、回调接口返回的字符串是否为success、订单状态是否被正确更新为已支付并完成发货。注意 URL 里的需要用引号包起来否则 shell 会把后面的参数截断。5.2 验证并发下不超卖用 wrk 打并发下单发卡平台的超卖是最难发现的隐性缺陷因为日常量小根本不会暴露。用 wrk 做一次简单的并发测试就能验证卡密锁定逻辑是否正确wrk -t 4 -c 50 -d 10s -s post.lua http://your-domain.com/api/order/create-t 4表示 4 个线程-c 50表示保持 50 个并发连接post.lua里写下单的 POST 参数和 Header。测试结束后检查订单表里相同商品的订单数是否等于锁定的卡密数再对比库存余量和已售数量。如果有超卖订单数会大于卡密数说明条件更新写错了或者没有在事务里执行。5.3 避坑回调地址必须是公网可访问这是最基础也是最容易踩的坑。免签接口的回调地址必须能被支付平台公网访问本机localhost或内网 IP 一律无效。开发调试时可以用内网穿透工具把本地端口暴露到公网但生产环境一定要部署到云服务器上。回调地址的域名不要频繁更换部分支付平台对新域名会有风控审核期更换域名可能导致回调被拦截。同一笔订单被回调多次是常态幂等判断不是可选项而是必选项。另外还要注意第三方个人支付平台的结算方式——有的平台支持 D1 自动结算到银行卡有的需要手动发起提现结算周期直接影响发卡平台的资金周转接入前要跟平台确认清楚。免签接口的调试关键在日志。回调接口里把$_GET、$_POST、验签结果、返回内容全部写入日志文件支付平台说“已发送回调”时你只需要看日志就能定位是哪一步断了。日志格式建议写成 JSON每条记录包含时间、订单号、事件类型和上下文排查问题时按订单号过滤即可。本文还有配套的精品资源点击获取