简介:这是一套面向站长、独立开发者及电商创业者的源码类虚拟物品销售系统,基于PHP与MySQL打造,内置文章收费、资源下载收费、游客购买、免签收款、三级分销、实名认证、佣金提现等机制,且除核心授权文件外全部开源,利于二次开发和快速上线。资源共400个文件,核心为324个PHP页面与逻辑模块,另有JS与CSS用于前端交互和界面样式,SQL与config文件负责数据库及运行参数初始化,整体压缩包仅1.99MB,结构精简、便于部署。压缩包还附带搭建教程,从环境准备、导入数据库到配置支付接口均有说明,可帮助技术基础一般的用户直接搭建一套可运营的付费下载站点。目前已有37人学习,适合希望低成本搭建虚拟商品交易平台、通过出售源码或数字内容获利的实践者参考。
1. 源码等虚拟物品销售系统:卖代码最耗时间的不是写,是发货
「源码等虚拟物品销售系统」这个方向,听上去是把代码打包往网上一挂收钱,实际干过的都知道,卖源码 90% 的时间耗在收款、发货、售后核对上。买家付款后你手动发网盘链接,一天三五单还行,等流量起来一天几十单,漏发、重发、对不上账、买家催问,每一件都在吃掉做项目的时间。标题里这套系统的目标很直接:从下单、支付接口回调到自动出下载链接或授权码,全链路无人值守。它适合单干接单、手里攒了模板插件想变现的开发者,也适合资源站和教程站运营者。先说明白一件事:它不解决「怎么把代码卖贵」,解决的是「别让重复劳动拖垮你」。
2. 选型与整体架构:先想清楚交付模型,再谈支付接口
不少新手上来就问「哪个发卡系统好用」,但发卡系统只是自动发货的一种实现。源码类虚拟物品和卡密不一样,卡密是一段字符串,源码是一个可能持续更新的项目集合。交付模型没定,支付接口接得再顺也白搭。这一章先把两条技术路线和三种交付方式讲清楚,再给能直接落地的最小架构。
2.1 两种常见方案:现成发卡系统 vs 自研订单中心
市面主流做法分两类。一类是直接部署开源的发卡系统,后台添加商品、设置价格、填好支付接口参数,付款后自动发卡密或自定义内容。优点非常明显:部署快,支付接口大多已经集成,一个人一个下午就能上线。缺点是交付逻辑被锁死在「发一串内容」的模型里,想做成「买家填邮箱后发送下载链接」「源码放在私有仓库自动开权限」这类场景,就得改源码或另挂服务。
另一类是自研订单中心,也就是自己写商品展示、创建订单、支付回调、发货逻辑。听起来重,但源码类商品恰恰需要这种自由度。你可以自己做授权码系统、临时下载链接、并发控制,也能在每个环节加日志和审计。坏处是工作量大,尤其在支付对账这块,踩坑成本不低。
| 对比项 | 现成发卡系统 | 自研订单中心 |
|---|---|---|
| 启动速度 | 当天可上线 | 1-2 周前后端联调 |
| 支付接口 | 已集成,换渠道要改配置 | 抽象接口后新增成本低 |
| 交付模型 | 卡密/文本为主 | 直链、授权码、仓库权限均可控 |
| 二次开发 | 受原系统约束 | 无约束 |
| 适合场景 | 纯卖卡密、简单源码包 | 多种交付、需售后授权管理 |
我一般建议:只卖一种打包好的源码,用现成发卡系统足够;打算长期做、要接入多种支付接口、要防倒卖做授权码,狠下心自研订单中心。这个标题既然强调「多种支付接口」,说明交付模型不会只有一种,自研路线后期收益更大。
2.2 虚拟物品的三种交付方式:直链下载、授权码、私有仓库
第一种直链下载最简单:买家支付成功后,系统生成一个一次性下载地址,点开就下载源码压缩包。成本低、体验好,但防不了倒卖——买家拿到压缩包后想怎么传就怎么传。适合低价模板、素材包、课程资料,这类东西本来也不指望防住。
第二种授权码:支付后系统生成一段唯一授权码,买家下载源码后,在本地运行激活脚本输入授权码才能解压或运行。好处是源码在传播前就有一道坎,不利于直接乱发,还能通过授权码反查订单。坏处是你必须维护一套激活校验逻辑,代码要写得够稳,否则买家会被激活流程劝退。
第三种私有仓库临时访问权:支付后系统创建或授权一个私有 Git 仓库的临时访问 token,买家通过 token 拉取代码,token 过期后还能续费。适合持续更新的源码项目,买家知道你会上新,复购和续费就顺理成章。技术门槛最高,需要处理 Git 仓库的权限控制,一般是小型团队才做得到。
选型没有绝对正确答案。我的判断标准是客单价:低于 50 元用直链,把成本压到最低;50-200 元用授权码;高价且承诺更新的项目,直接上私有仓库。交付模型定了,订单表和发货逻辑的字段才能定下来。
2.3 最小可跑架构:一张订单表和一个定时任务
不管前端用什么页面、后端用什么框架,源码销售系统的核心就三件事:订单、回调、发货。下面这张表是订单中心的起点,所有支付渠道的回调最终都落在这张表上。
CREATE TABLE virtual_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT '商户订单号,传给支付接口的唯一单号', product_id INT UNSIGNED NOT NULL COMMENT '商品ID,关联虚拟商品表', title VARCHAR(128) NOT NULL COMMENT '商品标题,下单时冗余保存', price DECIMAL(10,2) NOT NULL COMMENT '订单金额单位元,精确到两位小数', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已取消', pay_channel VARCHAR(20) NOT NULL DEFAULT '' COMMENT '支付渠道标识,如 pay_xxx', pay_trade_no VARCHAR(64) NOT NULL DEFAULT '' COMMENT '支付渠道的流水号', notify_count INT NOT NULL DEFAULT 0 COMMENT '回调通知次数,排查重复通知', callback_raw TEXT COMMENT '原始回调报文,脱敏后保存用于对账', created_at DATETIME NOT NULL, paid_at DATETIME NULL, delivered_at DATETIME NULL, UNIQUE KEY uk_order_no (order_no), UNIQUE KEY uk_channel_trade (pay_channel, pay_trade_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;order_no 是业务侧生成的唯一单号,每次下单都要保证不重复;uk_channel_trade 是防重复发货的关键约束,同一个支付渠道的同一笔流水只能出现一次。status 从 0 到 2 的顺序,让发货动作有明确的落点。callback_raw 字段用来存回调报文,但存入之前必须做脱敏,不能把支付密钥或敏感参数一起写进去。
再加一个每分钟执行的定时任务,扫描 status=0 且创建时间超过 30 分钟未支付的订单,统一标记为 3 取消。这个任务同时可以作为兜底对账:把已支付但未发货的订单捞出来告警。有了这张表和定时任务,系统已经能跑通最基础的下单-回调-发货闭环,后面再往上加支付渠道抽象和发货逻辑。
3. 支付接口接入:从下单到回调落库的完整链路
支付接口是这套系统里最容易出玄学问题的部分。表面上看是「发起支付、等回调」,实际坑在签名规则、金额单位、回调重复通知和状态幂等。这一章把下单、验签、回调处理三个环节拆开讲,每一段代码都对应一个真实落地动作。
3.1 生成支付订单:金额单位、签名和幂等键
下单接口要做的事是:校验商品存在、生成 order_no、查出价格、带上商品标题调用支付渠道的统一下单接口。这里第一个坑就是金额单位。有的渠道用元且保留两位小数,有的渠道用分;同一个系统里如果混用,对账时会出现莫名的几分钱差异。我会在代码层强制统一为「分」传给支付渠道,展示和订单表里用元。
大多数聚合支付渠道的签名规则大同小异:把除签名字段外的所有参数按 key 升序排列,拼成 key=value&... 形式,末尾连上商户密钥做 MD5,有的渠道用 HMAC-SHA256,但拼接思路完全一致。
function buildSign(array $params, string $secret): string { unset($params['sign']); ksort($params); $str = ''; foreach ($params as $k => $v) { if ($v === '' || $v === null) { continue; } $str .= $k . '=' . $v . '&'; } $str = rtrim($str, '&'); return strtoupper(md5($str . $secret)); }参数里像 extra、return_url 这类可能包含中文,拼接前要按渠道要求做编码或原样处理。order_no 生成用 uniqid 加随机后缀不够稳妥,我会专门写一个 20 位以上的数字串生成器,例如日期加自增序列加随机数,并设置数据库唯一约束兜底。签名字段名有的渠道叫 sign,有的叫 signature,接入前先看接口文档,别照抄。
下单后把支付平台的跳转链接返回给前端,让买家去完成付款。同时把 order_no 和支付渠道流水号存进订单表,这一步不更新支付状态,真正改状态的是回调。
3.2 异步回调校验:别把「收到回调」当「支付成功」
支付渠道的异步通知是可以伪造、可以重放、可以乱序到达的。回调处理的顺序必须是:验签、查订单、比对金额、幂等判断、更新状态并发货,最后向渠道返回成功标识。下面这段是回调处理的核心骨架:
function handleNotify(string $channel, array $payload): string { // 第一步验签,签名不对直接拒绝 if (!checkChannelSign($channel, $payload)) { logError('sign verify failed', $channel, $payload['order_no'] ?? ''); return 'fail'; } $orderNo = $payload['out_trade_no'] ?? ''; $tradeNo = $payload['transaction_id'] ?? ''; $paidAmount = $payload['amount'] ?? 0; $order = findOrderByNo($orderNo); if (!$order) { return 'fail'; // 订单不存在,返回失败让渠道重试 } // 金额比对用整数分或 bccomp,绝不用 float 直接比较 if (bccomp((string)$paidAmount, (string)$order['price'], 2) !== 0) { logError('amount mismatch', $orderNo); return 'fail'; } // 幂等判断:已支付或已发货直接返回成功 if ((int)$order['status'] >= 1) { return 'success'; } // 更新支付状态,然后执行发货 $db->beginTransaction(); try { markPaid($orderNo, $tradeNo, $payload); deliverVirtualGood($order); $db->commit(); } catch (Exception $e) { $db->rollBack(); return 'fail'; } return 'success'; }验签失败返回 fail,渠道会按间隔重试;订单不存在也返回 fail,避免渠道认为已经处理成功就不再来;金额不一致必须拦下,这是防止绕过的底线。幂等判断放在验签和金额之后:如果订单状态已经是已支付或已发货,说明前面处理过了,直接返回成功,让渠道停止重试。
这里强调一点:回调里以我方订单表里的金额为准,不能拿回调金额直接更新订单,更不能把回调金额当成订单金额展示给买家。所有金额落库前统一转成元,两位小数用 DECIMAL 存。回调处理成功返回的字符串必须是渠道约定的成功标识,通常是小写 success,不是 HTTP 200 就万事大吉。
3.3 接入多个支付渠道:把支付接口抽象成一个通道
标题说「多种支付接口」,落到代码上就是不同渠道的差异被隔离在一个接口后面。我只定义一个支付通道接口,每个渠道一个实现类,业务层只依赖接口:
interface PaymentChannel { // 创建支付订单,返回支付跳转信息 public function createPayOrder(string $orderNo, string $title, int $amountFen): array; // 校验回调,验签通过返回 true public function verifyNotify(array $payload): bool; // 主动查询订单状态,用于对账和定时补偿 public function queryOrder(string $orderNo): array; }金额参数我用 int 类型传入,单位是分,避免浮点数误差。每个渠道实现类里各自处理签名算法、字段命名和回调解析,然后注册到一个通道管理器里,通过配置文件把 pay_channel 标识映射到对应实现。新增渠道时,写一个新实现类,配置加一行,业务代码不用动。
这样做最大的好处是对账方便。一个运行中的销售系统,支付渠道不止一个,有回调丢失、延迟、金额不一致时,可以写一个统一的对账脚本,遍历所有启用渠道的 queryOrder 接口,和本地订单表做比对。渠道抽象这件事,宁可前期多花半天,也别等到接第三个渠道时回头重构。
4. 自动发货实现:把源码安全地交给买家
支付回调处理完,订单变成已支付,紧接着就是发货。源码类商品发货不是把文件路径发给买家那么简单,要考虑下载链接有效期、防止被抓包预测、授权码与源码的联动、以及售后需要的操作记录。这一章给三种发货方式的落地实现和参数设置。
4.1 一次性下载链接:随机 token、过期时间和删除
直链下载是最常用的发货方式,核心逻辑是支付成功后生成一个随机 token,存进缓存并设置过期时间,买家点击下载时校验 token、校验订单状态、输出文件流后立即删除 token。
$token = bin2hex(random_bytes(32)); $cache->setex('dl_token:' . $token, 600, $orderNo); $downloadUrl = 'https://example.com/deliver?token=' . $token;token 必须用加密安全的随机源生成,不能是时间戳加 MD5 这种可预测的值,否则会被扫链接的工具试出来。过期时间 600 秒只是示例,可以按商品大小调:源码包小于 100MB,10 分钟足够;超大压缩包建议 30 分钟。下载接口的处理逻辑:
$token = $_GET['token'] ?? ''; if (!preg_match('/^[a-f0-9]{64}$/', $token)) { exit('invalid token'); } $orderNo = $cache->get('dl_token:' . $token); if (!$orderNo) { exit('link expired'); } $order = findOrderByNo($orderNo); if ((int)$order['status'] < 1) { exit('order not paid'); } $filePath = productFilePath($order['product_id']); header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="source_' . $order['product_id'] . '.zip"'); header('Content-Length: ' . filesize($filePath)); readfile($filePath); $cache->del('dl_token:' . $token);正则校验 token 长度和字符集,先挡住非法输入;从缓存取到 orderNo 后再查订单状态,避免有人拿一个有效 token 去试未支付订单。文件流用 readfile 在小文件场景足够了,超过 200MB 建议用 Web 服务器的 X-Sendfile 机制,让服务器直接发送文件,避免 PHP 进程占内存。下载完成后删除 token,保证一次性,这也是避免源码被多次抓取的基本手段。
token 过期但订单已支付时,不能让买家白花钱。常见做法是下载失败后提供一个「重新获取下载链接」的按钮,前提是订单已支付,并且重置 token 时做频率限制,比如 5 分钟才能重新生成一次。这一步直接关系到售后体验,漏了会多出一堆客诉。
4.2 授权码与源码内的小型校验脚本配合
授权码模式比直链多一道校验:买家下载的压缩包里是一份加密或混淆过的源码,需要输入授权码才能完成激活。生成授权码的代码可以写得很简单:
$code = strtoupper(bin2hex(random_bytes(8))); $order = findOrderByNo($orderNo); $db->insert('license_code', [ 'order_id' => $order['id'], 'product_id' => $order['product_id'], 'code' => $code, 'status' => 0 ]);8 字节随机数转十六进制后是 16 位字符,长度适中,不容易撞码。授权码存进独立的 license_code 表,一个订单可以生成多个,但要限制总数防止滥用。源码压缩包内提供一个 activate.php(PHP 类源码最常见),买家本地运行php activate.php <授权码>,脚本内部通过 HTTP 请求你的激活接口完成校验。
激活接口返回的内容要简洁,只返回成功或失败和过期时间,不要把授权码对应的订单信息原样返回。还要考虑离线激活的需求:把授权码做成「订单号+密钥」的签名串,源码内置公钥验证签名,这样不联网也能激活。离线激活省去服务器接口压力,代价是公钥会被分析,所以这种模式一般配合定期联网校验一起用,效果更好。
授权码的防倒卖效果很有限,它防的是「把压缩包直接甩给别人」的顺手行为,不是专业破解。理解这个边界,才不会被买家投诉搞崩心态。
4.3 发货记录、审计与售后查询
发货是一次性动作,但售后是长期需求。买家会问:我付钱了链接在哪、下载到一半断了怎么办、授权码能不能换台机器。没有操作记录,这些问题全靠人工猜。我维护一张发货日志表,每次发货、重新生成链接、授权码激活都写一条:
CREATE TABLE delivery_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, product_id INT UNSIGNED NOT NULL, action VARCHAR(20) NOT NULL COMMENT 'deliver / reissue / activate', ip VARCHAR(45) NOT NULL COMMENT '买家IP,仅做风控', user_agent VARCHAR(255) NOT NULL DEFAULT '', created_at DATETIME NOT NULL, KEY idx_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;action 字段区分发货、补发、激活,查询售后问题时按 order_no 拉出时间线就行。IP 和 user_agent 仅用于识别异常行为,比如同一个人下单后两秒内换十几个 IP 激活授权码,基本可以判定在批量试码。日志只保留业务所需字段,不要整段记录支付回调原文,这是底线。
从这一章可以看出,自动发货并不复杂,难的是把边界场景想全。下载链接失效要能补、授权码要能溯源、操作要能审计,这三件事做到,售后压力能降一大半。
5. 源码销售系统避坑:五个一踩一个准的实战教训
5.1 订单已支付但下载链接失效
现象:买家付款后点下载,页面提示「链接不存在或已过期」,重复几次都一样。
原因:支付回调里先改了订单状态为已支付,再生成下载 token,但 token 写入缓存时失败或者 token 的 key 拼写和下载接口不一致,导致下载时查不到。还有一种情况是下单时提前生成了 token,订单未支付就过期,支付完成后没有重新生成。
解决:把 token 生成和订单状态更新放进同一个事务里,状态到已支付才允许生成 token;下载接口做兜底,校验到 token 不存在时,如果订单已支付,自动重新生成一个 token 并返回新的下载地址,而不是直接报死。
5.2 回调重复通知导致重复发货
现象:买家收到两条下载地址,后台发货记录有两条,授权码生成了两个。
原因:支付渠道因为网络原因重发回调,系统处理回调时没有做幂等,第二次回调进来又执行了一次发货逻辑。也有人遇到的是渠道回调乱序,第一次回调先到把状态改成已支付,第二次正常回调再走一遍发货。
解决:利用订单表 pay_channel + pay_trade_no 唯一约束做数据库层兜底;代码层在发货前检查 status,已发货直接返回成功;更稳妥的是把发货动作放在事务里,用UPDATE ... WHERE status=1更新影响行数作为是否已发货的判断条件。
5.3 下载链接被批量扫描薅走
现象:同一天内下载量突然爆增,日志里出现大量不同 IP 访问同一个下载地址,或者 token 被反复猜测。
原因:token 长度太短或生成方式可预测;另一个常见问题是下载地址出现在订单完成页的源码里,被爬虫抓去后批量尝试。低价源码被整个打包流出,往往就是这么开始的。
解决:random_bytes 生成 32 字节 token;下载接口加频率限制,同一 IP 每分钟最多请求 5 次;订单完成页不要把 token 直接放在页面源码里,改为按钮点击后通过接口动态获取;一旦下载成功立即删除 token,不给批量机会。
5.4 金额精度对不上账
现象:订单表显示买家付了 19.90 元,支付渠道账单显示 19.90 元,但对账脚本总是提示差异,差 0.01 元。
原因:下单时金额用浮点数计算,比如 9.95 + 9.95 在浮点运算下变成 19.899999,传给支付渠道被四舍五入成 19.90,回调比对时又用 float 直接比较,精度不一致导致误判。
解决:数据库用 DECIMAL(10,2),PHP 里金额全用整数分计算,回调比对用 bccomp 或强转字符串比较。记住一条铁律:金额永远不要用 float 类型做相等判断。
5.5 日志里留下不该有的敏感信息
现象:排查线上问题时翻日志,发现回调报文被完整打印到日志文件里,里面有商户密钥、签名参数等敏感内容。
原因:为了方便调试,直接把整个请求体var_dump到日志,上线后忘了关。日志文件一旦泄露,攻击者可以直接伪造回调请求。
解决:日志统一走一个方法,只记录 channel、order_no、trade_no、金额、状态这几个业务字段;密钥永不写日志,更不能入数据库;定期做日志脱敏检查,敏感字段用掩码替换后再落盘。
6. 从「能跑」到「敢卖」:验证清单与上线前的一小时测试
系统写完了,别急着挂出去。我每次上线新的支付渠道或改动发货逻辑,都会走一轮固定的验证流程,顺序不能乱。第一验证回调重放:把同一份回调报文连发两次,第二次系统必须直接返回成功且不产生第二条发货记录。第二验证金额篡改:把回调里的金额改大、改小、改成负数,系统都要拒绝并记录日志。第三验证下单成功但回调丢失:手动把订单置为已支付状态,再触发一次发货,确认定时对账不会发重复货。第四验证 token 过期:等 token 过期后再下载,确认提示友好,以及重新生成链接的频率限制正常。第五验证并发:同一商品同时下 20 单,检查 order_no 无重复,数据库唯一约束不报错。
这套验证建议写成一份简单的测试脚本,每次上线前自动跑一遍,而不是靠人点。回调重放和金额篡改这两个场景最值得自动化,因为人肉测很容易漏。
上线投入运营后,我还会保留两个日常习惯。第一个是每天凌晨跑一次渠道对账,用每种支付渠道的查询接口拉取昨日流水,和本地订单表比对,把差异订单列出来单独处理。第二个是盯回调通知次数,notify_count 异常偏高的订单说明渠道重试了多次,可能是回调处理逻辑里一直在返回 fail,需要查代码。
这个系统的核心其实不是支付接口接得多花哨,而是每一笔订单都能从下单到发货对得上账。带货不翻车的秘诀很简单:把人工操作的地方尽量自动化,把自动化失败的地方尽量留下日志。希望帮到你。
本文还有配套的精品资源,点击获取