简介:一套面向站长和开发者的自动发卡网H5源码,基于ThinkPHP框架构建,可直接对接亿乐社区,用于快速搭建支持数字商品售卖、自动发货、订单管理的小型交易平台。安装教程覆盖域名解析、宝塔主机环境配置、运行目录修改、伪静态规则及后台入口,技术门槛适中,适合已有服务器使用经验、希望快速上线发卡业务的个人或团队。资源压缩包大小约28.05MB,解压后共包含2000个文件,其中以PHP后端逻辑、JS前端交互、HTML页面、CSS/LESS样式为主,也涵盖PNG图片、JSON配置、Markdown说明及SQL数据库脚本等,前后台界面与功能代码完整,便于二次开发和界面调整;通过源码目录可快速定位支付接口、订单处理、用户中心等模块。目前已有724人学习下载,社区实践反馈较好。结合亿乐社区对接配置,可在较短时间内打造一个可运营的自动发卡网站,同时也可作为ThinkPHP全栈项目的实战学习素材。
1. 红盟自动发卡网 H5 网站源码解决的是哪一段生意链路
做虚拟商品销售的人都有类似的经历:单量不多但琐碎,买家要的是付款后立刻拿到结果。靠人工发卡,一天几十单就开始算错账;挂到电商平台,又受不了抽成和审核节奏。红盟自动发卡网 H5 网站源码解决的正是这个环节:商品、订单、支付回调、卡密发放全部自动化,用户用手机打开页面就能完成购买,支付确认后系统立刻下发卡密。标题里的“对接亿乐社区”也值得展开——亿乐社区承担用户池的角色,发卡网通过接口把社区登录态、余额或订单同步过来,用户不需要在发卡站重新注册。这篇内容适合准备搭移动端发卡渠道的个人开发者、接手发卡源码的运维和想从 PC 端迁移到 H5 形态的现有站主,按部署、对接、验证的顺序给出能直接操作的命令和判断标准。
2. 先看懂发卡网运转模型:H5 订单链路的四个核心对象
发卡网和普通商城最大的差别在交付物:商城卖的是物流履约,发卡网卖的是“一串卡密”。卡密在买家完成支付之前不允许泄露,所以整个系统的状态流转本质上是订单状态机决定发卡时机,而不是某个定时任务批量发货。先把运转模型看明白,再装环境能省掉后面大部分排错时间。
2.1 商品、订单、卡密、回调四个对象的职责边界
一张发卡网的数据模型不管换什么皮肤,核心都离不开四个对象:商品、订单、卡密、支付回调。它们的关系定义清楚后,安装后该改哪些表、对接时该往哪个接口挂逻辑,就都顺了。
| 数据对象 | 核心字段 | 在一次购买里承担的工作 |
|---|---|---|
| 商品 goods | id, name, price, category_id, status | 决定“卖什么”,分类页和商品详情页读这张表 |
| 订单 orders | order_no, goods_id, amount, pay_channel, status | 唯一记录一次购买的全过程,status 是状态机的当前值 |
| 卡密 cards | id, goods_id, content, order_no, status | 库存载体,只有被订单绑定并标记已发后,买家才能看到内容 |
| 回调记录 pay_callbacks | order_no, channel_trade_no, raw_data, status | 记录支付渠道每一次通知,是排查掉单的第一现场 |
重点说两个容易混淆的点。卡密表里的 order_no 在没有售出时是空字符串,不能当成“已使用”;只有 status 字段从 unsold 变成 issued,页面才允许展示卡密内容。另一个是回调记录:支付网关的通知可能到达多次,同一订单的回调必须幂等,否则同一张卡会被发两次。后面第 4 章写签名和幂等时会专门展开。
2.2 H5 与 PC 端在订单链路上的三个差异
PC 端发卡网的常规做法是跳转收银台扫码支付,用户全程在一个浏览器标签页里完成,支付完成立刻刷新订单页。H5 场景多了不可控因素:页面所在环境不一定是完整桌面浏览器,可能是微信内置浏览器;用户拉起支付后可能切走再回来。这两点决定了 H5 不能依赖“支付成功后页面自动刷新”,必须用主动查询收口。
常见做法是下单成功页放一个轮询器,每 2 秒查一次订单状态,查到已支付就跳到取卡页。服务端提供一个状态查询接口,返回订单状态和卡密发放进度。
// 下单页面的轮询逻辑 const orderNo = new URLSearchParams(location.search).get('order_no'); function queryOrderStatus() { fetch('/api/order/status?order_no=' + orderNo, { headers: { Accept: 'application/json' } }) .then(res => res.json()) .then(data => { // 服务端返回 paid 表示订单已确认且卡密已分配 if (data.code === 0 && data.data.status === 'paid') { clearInterval(timer); location.href = '/order/detail?order_no=' + orderNo; } }) .catch(() => { // 网络抖动时静默失败,等下一次轮询,不要在这里弹重试窗 }); } const timer = setInterval(queryOrderStatus, 2000);轮询间隔设 2 秒而不是 1 秒,是为了给支付回调留写入时间,同时降低对数据库的瞬时压力。如果商品客单价低、用户密集,可以把间隔改成 3 秒,配合 Nginx 对该接口做缓存或限流,服务器压力会小很多。这里的关键是查询接口要查数据库实时状态,不能查缓存里的旧快照,否则会出现“已支付但页面还显示待支付”的假象。
2.3 源码目录怎么读:配置、控制器、视图各管什么
红盟这一系的发卡源码通常按 PHP 的 MVC 习惯组织,不同作者改出来的目录名不一样,但结构上有共性:一个 .env 文件装数据库和支付密钥,一个 controller 目录处理下单、支付回调、发卡,一个 view 或 template 目录放 H5 页面。装完之后最重要的不是背目录,而是搞清楚两个文件的路径:站点配置文件和支付/对接回调入口。
用通用结构估一个典型布局,实际以你下载的压缩包为准:
project-root/ ├── .env # 数据库、支付、对接密钥,安装时只改这里 ├── index.php # 入口文件,伪静态全部转发到这里 ├── app/ │ ├── controllers/ # OrderController.php 等 │ ├── models/ # 商品、订单、卡密的模型 │ └── services/ # 对接第三方社区的封装 ├── storage/logs/ # 运行日志,排错第一现场 └── install/ # 安装向导,装完必须删除安装向导目录 install 装完必须整个删掉或改成访问受限,否则等于把重装入口留给别人。这个细节影响整站安全,尤其是发卡网这种有资金流动的系统。
3. 安装部署:从空服务器到 H5 发卡站跑通
发卡网因为包含支付回调,对环境和域名要求比普通展示站严格。如果手上只有一台之前挂博客的机器,先确认 PHP 版本和扩展再决定是否复用,不要直接传源码覆盖。
3.1 环境参数与 PHP 扩展检查
红盟系发卡网 H5 源码基本都是 PHP + MySQL 的组合,部分二次开发版引入 Redis 做订单缓存。部署前先确认下表,环境不对的话安装向导会在第一步报错。
| 环境项 | 推荐值 | 说明 |
|---|---|---|
| PHP | 7.4 ~ 8.1 | 8.2 之后的动态属性用法可能触发 deprecated 报错 |
| MySQL | 5.7+ 或 MariaDB 10.3+ | 必须选 utf8mb4,否则 emoji 商品名入库变问号 |
| Web 服务器 | Nginx 1.18+ | 使用 PHP-FPM 模式,伪静态规则见 3.2.2 |
| PHP 扩展 | pdo_mysql, curl, openssl, fileinfo, mbstring | fileinfo 缺失时很多安装包拒绝继续 |
| 伪静态 | 必须配置 | 路由全靠 rewrite,不配就是 404 |
用 root 登录服务器后,先跑这段命令确认扩展齐全:
php -v php -m | grep -E 'pdo_mysql|curl|openssl|fileinfo|mbstring'如果php -m列出的结果缺了其中任一项,用包管理器补装。以 CentOS/RHEL 系为例:
yum install -y php-mysqlnd php-curl php-openssl php-mbstring systemctl restart php-fpm nginxDebian/Ubuntu 系把 yum 换成 apt,包名通常带 php8.x- 前缀。装完再跑一次php -m,确认输出里有全部五项再继续。
3.2 安装四步走:上传、建库、配置文件、伪静态
第一步,上传源码。把压缩包传到站点根目录后解压,并调整目录属主,否则安装向导往 storage 目录写配置时会提示没有权限:
unzip redmoon-h5.zip -d /var/www/redmoon cd /var/www/redmoon chown -R www:www storage config .env.example 2>/dev/null chmod -R 755 storage第二步,建数据库。发卡网的数据量涨得比普通博客快,订单表和卡密表很容易到百万行,建议建库时直接指定 utf8mb4 和独立账号:
mysql -uroot -p CREATE DATABASE redmoon DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'redmoon'@'localhost' IDENTIFIED BY '请换成一个强密码'; GRANT ALL PRIVILEGES ON redmoon.* TO 'redmoon'@'localhost'; FLUSH PRIVILEGES;说明:独立数据库账号的意义在于给程序最小权限,只开放这一个库的权限,避免发卡网站被入侵后拖走同服务器上的其他库。这一步不要省。
第三步,填写配置文件。安装包一般提供 .env.example,复制成 .env 后只改下面几行:
DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=redmoon DB_USERNAME=redmoon DB_PASSWORD=你的强密码 # H5 站点对外地址,支付回调会基于这个地址拼接 APP_URL=https://mall.yourdomain.com # 亿乐社区对接配置,安装时可先留空 YILE_API_BASE= YILE_APP_ID= YILE_SECRET= YILE_NOTIFY_URL=${APP_URL}/api/yile/notify第四步,配置伪静态。Nginx 站点配置里加这段:
server { listen 80; server_name mall.yourdomain.com; root /var/www/redmoon; index index.php; location / { # 文件或目录真实存在则直接访问,否则交给 index.php 处理 try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }try_files这行的作用是:当请求路径找不到对应文件时,把路由交给 index.php,由 PHP 侧的控制器决定返回哪个页面。漏掉这一行,H5 页面打开会大面积 404。
3.3 安装完先做三项探针
第一项,确认安装向导能正常访问并写入数据表。浏览器打开https://mall.yourdomain.com/install,按提示填数据库信息。填写时注意站点地址统一走 https,回调地址不要 http 和 https 混用。
第二项,确认伪静态对后台路由有效。打开后台登录页,能正常显示而不是 404,说明 rewrite 生效。
第三项,检查日志目录能否写入:
touch /var/www/redmoon/storage/logs/test.log && rm /var/www/redmoon/storage/logs/test.log echo "log writable"三项都过了,发卡网本身就跑通了。先别急着上架商品,下一步把亿乐社区对接好,不然账号体系和支付后的取卡流程是断的。
4. 对接亿乐社区:登录态、余额冻结与回调验签
对接亿乐社区的本质,是解决两个系统之间的主数据不一致问题。用户在社区注册过,不应该在发卡网再注册一遍;用户在社区里有余额或积分,也不该在发卡网另开一张充值表。对接工作主要分三块:登录态识别、余额处理、回调同步。
4.1 对接前先确认三个问题
社区对接不是贴个链接就完事。开工前先把下面三个问题写清楚,写不清楚的找社区方要接口文档核对:
| 问题 | 默认方案 | 需要确认的点 |
|---|---|---|
| 用户怎么识别 | 社区授权后返回 open_id | open_id 是否全局唯一,换绑后会不会变 |
| 余额在哪边扣 | 锁定社区余额,支付成功后再真正扣减 | 社区接口支持锁定额度还是只支持直接扣 |
| 订单结果怎么同步 | 发卡网主动 POST 到社区回调地址 | 社区对回调签名要求是什么算法 |
第一版建议选“社区余额锁定 + 本地订单流转”的方案。用户发起订单时,本地先写入未支付订单,再调用社区锁定额度接口;支付确认后,本地把订单置为已支付并立刻发卡。即使社区响应慢,订单最终状态也由本地兜底。
4.2 登录态打通:code 换 token,token 换用户
H5 页面不能直接拿社区密码,常规做法是跳转到社区的授权地址,社区确认登录后带着临时 code 跳回来。发卡网拿到 code 后换取访问令牌,再拿令牌访问用户资料接口,拿到 open_id 作为本地用户唯一标识。下面的代码用 YILE_API_BASE 代表你在社区后台拿到的接口根地址,实际路径以社区文档为准。
<?php // 亿乐社区授权回调入口:/oauth/callback.php $code = $_GET['code'] ?? ''; $tokenUrl = YILE_API_BASE . '/oauth/token'; $tokenData = httpPost($tokenUrl, [ 'app_id' => YILE_APP_ID, 'code' => $code, 'secret' => YILE_SECRET, ]); if (empty($tokenData['access_token'])) { exit('授权失败,请确认 app_id 和 secret 是否正确'); } $userUrl = YILE_API_BASE . '/user/info'; $userData = httpPost($userUrl, [ 'access_token' => $tokenData['access_token'], ]); $openId = $userData['open_id'] ?? ''; if ($openId === '') { exit('未取到社区用户标识'); } // 本地用户表以 open_id 建唯一索引,登录成功则复用,没有则注册 $user = findUserByOpenId($openId); if (!$user) { $user = createUser([ 'open_id' => $openId, 'nickname' => $userData['nickname'] ?? '' ]); } // 把登录态写进本地 session 或 JWT,供后续下单接口使用这段代码有三个要点。第一,token 请求只能发到服务端,不能放到 H5 页面里让浏览器直接请求,否则 secret 会被别人在控制台看到。第二,本地用户表要给 open_id 建唯一索引,避免并发回调时重复创建两个用户。第三,社区返回的 open_id 不要和订单号混着用,订单关联用户时只存本地表主键 user_id。
4.3 余额支付:冻结、确认扣减、超时解冻
发卡网里最怕的是“社区扣了钱,本地没发卡”。用两个系统各自的接口硬拼,一定会出现不一致。常见做法是冻结/锁定模式,社区侧提供三个接口配合使用,本地流程这样编排:
订单创建 → 调用社区冻结接口锁定金额 → 支付成功回调 → 调用社区确认扣减 → 本地发卡。如果用户支付中途放弃,超过超时时间后再调用社区解冻接口释放金额。
<?php // 创建订单后,锁定社区余额 $freezePayload = [ 'user_id' => $user['open_id'], 'order_no' => $orderNo, 'amount' => $orderAmount, // 超时时间,默认 15 分钟后自动解冻 'expire_time' => time() + 900, ]; $freezePayload['sign'] = makeSign($freezePayload, YILE_SECRET); $freezeResult = httpPost(YILE_API_BASE . '/wallet/freeze', $freezePayload); if (!$freezeResult['success']) { // 余额不足或冻结失败,直接提示用户,不进入支付流程 exit('额度冻结失败,请检查社区余额'); }这里要处理两个边界才能提交单。一个是订单超时要在本地定期扫描,把超过 15 分钟还没支付的订单调一次社区解冻;另一个是用户支付期间关闭了页面,支付流程中断,冻结额度会一直占用,同样靠这个超时任务收口。社区接口没有自动超时的,必须在本地定时任务里补偿。
4.4 签名校验与回调幂等,少一个都会出事
亿乐社区回调和发卡网接收回调之间没有任何会话,消息是纯 HTTP POST。双方必须约定签名规则,否则任何人都能伪造“支付成功”回调来白拿卡密。最常见也最省事的方案是:参数按 key 升序排列,拼成 key=value&key2=value2,尾部追加 secret,取 MD5 大写。
<?php function makeSign(array $params, string $secret): string { // 过滤空值,避免签名结果不稳定 $params = array_filter($params, fn($v) => $v !== '' && $v !== null); ksort($params); $str = http_build_query($params) . $secret; return strtoupper(md5($str)); }接收回调的控制器里,先验签再验订单状态,两步顺序不能反:
<?php $notify = $_POST; $sign = $notify['sign'] ?? ''; unset($notify['sign']); // 第一步:验签名 if ($sign !== makeSign($notify, YILE_SECRET)) { http_response_code(401); exit('invalid sign'); } // 第二步:验订单状态和金额 $order = findOrderByNo($notify['order_no']); if (!$order || $order['status'] !== 'unpaid') { // 重复回调和未知订单都统一当成功返回,避免社区反复重发 exit('success'); } if ((string) $order['amount'] !== (string) $notify['amount']) { http_response_code(400); exit('amount mismatch'); } // 第三步:确认扣减社区余额并标记本地已支付 confirmDeduct($order['order_no']); releaseCard($order); echo 'success';回调必须输出固定内容让社区识别。很多对接失败的案例,都是因为回调端返回了 HTML 错误页,社区的程序判断非约定内容就标记失败并不断重试。注意第二步里“重复回调”也返回 success,是为了终止重试,不要把重复回调当成错误去大量告警。
提示:工具函数 httpPost 和 findOrderByNo 是项目里已有的封装,按实际项目的函数名替换即可。核心逻辑在于签名算法和回调状态机,这两段不要省。
5. 上线前必测的一步:本地伪造亿乐社区回调验证全链路
这一节给的是上线前必做的一个验证动作,同时也是排查“社区已扣款但用户没收到卡”这类事故的通用办法。装上源码不等于能发卡,只有把伪造回调这条路走通,才算真正完成对接。
5.1 用 curl 生成签名并调用回调地址
先在数据库里把一条测试订单的 status 改成 unpaid,订单号记下来,然后拿着订单号和金额去调回调地址。签名规则和上一章完全一致,只是这次用命令行一行的 bash 完成:
ORDER_NO="20250501001001" AMOUNT="19.90" SECRET="你在 .env 里配的 secret" # 按升序拼接待签名串,再追加密钥 SIGN_STR="amount=${AMOUNT}&order_no=${ORDER_NO}${SECRET}" SIGN=$(echo -n "$SIGN_STR" | md5sum | awk '{print toupper($1)}') curl -X POST "https://mall.yourdomain.com/api/yile/notify" \ -d "order_no=${ORDER_NO}" \ -d "amount=${AMOUNT}" \ -d "sign=${SIGN}"如果一切正常,curl 会拿到success字样,数据库里该订单 status 变为 paid,对应卡密变为已发。这个 bash 脚本能重复使用,每次只替换订单号和金额,就是在验证一个完整的发卡流程。
5.2 单独的回调日志和 5 分钟补单任务
发卡网排错最痛苦的情况是“支付渠道说没回调、社区说已扣款、本地说没订单”。应对办法是给回调单独写日志,每次收到回调都记录原始报文和处理结果,再补一个 5 分钟扫描未支付订单的任务:
<?php // cron 每 5 分钟执行一次 $rows = query("SELECT * FROM orders WHERE status='unpaid' AND created_at < NOW() - INTERVAL 15 MINUTE"); foreach ($rows as $row) { // 调社区解冻,释放占用额度 unfreezeCommunityBalance($row['order_no']); }日志写到storage/logs/yile_notify.log,每行固定格式:时间、订单号、金额、签名校验结果、处理动作。配合前面的 curl 伪造回调,出现问题时先看日志判断是没收到、还是收到后验签失败,能把排查范围从整个代码库缩小到签名算法和回调参数格式。这个习惯对发卡网长期运维的价值,远高于多写两个监控页面。
本文还有配套的精品资源,点击获取