轻量级PHP发卡系统:免签支付对接与自动发货实现
2026/9/16 16:50:56 网站建设 项目流程

简介:这是一套面向开发者与个人站长的在线虚拟商品自动交易发卡平台源码,适用于游戏点卡、会员账号、软件授权等数字商品的无人值守式销售与发货场景。系统基于ThinkPHP5框架与Layui2.2构建,支持PC与移动端自适应,集成第三方个人免签支付(支付宝/微信),彻底去除后门,全开源可二次开发。压缩包共2000个文件,主体为1083个PHP业务逻辑文件、229个GIF/PNG/JPG等静态资源、58个HTML与JS前端页面及交互脚本,另有SQL数据库结构、YML配置、JSON接口定义等配套文件,整体体积13.94MB,结构清晰便于模块化学习与部署。已有70人下载学习,提供完整安装教程、默认后台入口(/admin.php)及初始化账号密码,含数据库导入、配置修改等实操指引,适合具备PHP+MySQL基础的中级开发者快速搭建私有发卡平台并理解自动发货核心流程。

1. 这不是电商后台,而是一套面向虚拟商品交付闭环的轻量级发卡系统:它用 PHP 实现免签支付对接、自动发货、订单状态同步,不依赖微信/支付宝官方商户资质,适合个人开发者快速上线数字内容、激活码、会员时长等无物流商品

你不需要营业执照,也不用走平台审核——只要能部署 PHP 环境,就能跑通从用户下单、支付回调、库存扣减到邮件/SMS/站内信自动发卡的全链路。它不是 SaaS 平台,而是可私有化部署的源码级解决方案;不是“免签约”噱头,而是真实复用第三方聚合支付通道(如部分支持个人主体接入的聚合网关)完成资金归集与异步通知;不是静态页面,而是基于原生 PHP + MySQL 构建的最小可行交易内核。典型适用场景包括:独立知识付费课程密钥分发、游戏点卡批量售卖、API 接口调用额度充值、小程序虚拟会员开通。对运维要求极低——宝塔面板一键部署即可运行,web.config仅用于 IIS 兼容层,核心逻辑全部落在index.phppay_callback.phpsend_card.php三个主文件中。本文不讲合规边界,只聚焦技术实现:如何让这套源码在真实服务器上稳定触发发货、避免重复回调、防止库存超卖,并把支付结果准确映射到用户账户。

2. 搭建环境与核心文件结构解析:从宝塔部署到 web.config 兼容配置,明确每个 PHP 文件的职责边界

2.1 宝塔面板下 PHP 环境的最小化配置要求

该系统对 PHP 版本敏感度高,必须使用 PHP 7.4 或 8.0(不兼容 8.1+),原因在于其支付回调验签逻辑大量使用openssl_verify()的旧版参数签名方式,且部分字符串处理函数(如mb_convert_encoding())在 8.1 中默认行为变更会导致验签失败。安装时需手动关闭disable_functions中的execshell_execproc_open(用于异步发卡脚本调用),并开启opensslcurlmbstringjson扩展。MySQL 推荐 5.7,字符集统一设为utf8mb4_unicode_ci,否则中文订单备注会乱码。数据库导入后,务必检查users表中pay_status字段是否为TINYINT(1)类型,这是后续状态机驱动发货的核心字段。

2.2 源码目录结构与关键文件职能拆解

整个系统无框架依赖,采用过程式 PHP 编写,主目录结构如下:

├── /admin/ # 后台管理(含发卡记录、订单查询、库存调整) ├── /api/ # 对接第三方支付的回调入口(pay_callback.php 是核心) ├── /config/ # 数据库连接、支付密钥、发卡模板配置 ├── /includes/ # 公共函数库(db.php 数据库操作、utils.php 加密/日志/时间处理) ├── index.php # 前端商品展示与下单页(含表单提交至 /api/pay.php) ├── send_card.php # 发货主逻辑(读取待发货订单 → 生成卡密 → 更新状态 → 推送通知) └── web.config # IIS 服务器重写规则(将 /pay/xxx 转发至 /api/pay_callback.php)

提示web.config不是必需文件,仅当部署在 Windows IIS 环境时才生效;Linux/Apache/Nginx 用户应忽略此文件,改用.htaccessnginx.conf配置 rewrite 规则,将所有/pay/*请求代理至/api/pay_callback.php。若误启用web.config且未配置 IIS,会导致 500 错误。

2.3pay_callback.php的三重校验机制与状态流转设计

该文件是支付成功后的唯一入口,承担验签、查单、发货三大任务。其执行流程严格遵循原子性原则:

// 1. 获取原始 POST 数据(非 $_POST,因部分网关用 raw body 提交 JSON) $raw_data = file_get_contents('php://input'); $data = json_decode($raw_data, true); // 2. 三重校验:① 商户号匹配 ② 时间戳防重放(15分钟窗口)③ 签名验签(RSA-SHA256) if (!verify_mch_id($data['mch_id']) || !is_valid_timestamp($data['timestamp']) || !verify_sign($raw_data, $data['sign'], $config['private_key'])) { exit('fail'); // 必须返回纯文本 'fail',否则网关持续重试 } // 3. 查询本地订单是否存在且状态为 'unpaid' $order = get_order_by_out_trade_no($data['out_trade_no']); if (!$order || $order['pay_status'] != 0) { exit('success'); // 已处理过,直接返回 success 避免重复发货 } // 4. 更新订单状态并触发发货 update_order_status($order['id'], 1); // pay_status=1 表示已支付 dispatch_send_card_job($order['id']); // 异步调用 send_card.php exit('success');

注意exit('success')是硬性协议要求,任何其他响应(如 JSON、HTML)都会导致网关判定回调失败并反复投递。dispatch_send_card_job()函数内部使用curl后台静默调用send_card.php?id=xxx,避免阻塞支付通道响应时间。

3. 免签支付接口对接实操:以主流聚合通道为例,详解参数组装、签名生成与回调调试技巧

3.1 第三方聚合通道选型与密钥注入位置

所谓“免签”,本质是接入已获支付牌照的聚合服务商(如某付通、某易付等),其提供面向个人经营者的简易接入通道。这些通道不要求营业执照,但需实名认证+银行卡绑定。对接时需在/config/config.php中填入:

return [ 'pay_gateway' => 'fuyou', // 可选:fuyou / yee / tenpay_personal 'mch_id' => 'MCH_123456789', // 商户号 'api_key' => 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6', // API 密钥(非 RSA 私钥) 'private_key' => "-----BEGIN RSA PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...\n-----END RSA PRIVATE KEY-----", // 用于验签的私钥 'notify_url' => 'https://yourdomain.com/api/pay_callback.php', // 回调地址(必须备案域名) ];

提示private_key必须是 PEM 格式 RSA 私钥(2048位),不能是公钥或证书。若使用在线工具生成,需选择“PKCS#1”格式而非“PKCS#8”,否则openssl_verify()会返回 false。

3.2 下单请求构造:index.php中的表单提交与加密参数生成

前端下单不直连支付网关,而是先提交至/api/pay.php统一处理:

<!-- index.php 中的商品表单 --> <form action="/api/pay.php" method="post"> <input type="hidden" name="goods_id" value="1001"> <input type="hidden" name="price" value="9.90"> <input type="hidden" name="user_id" value="<?php echo $_SESSION['uid']; ?>"> <button type="submit">立即购买</button> </form>

/api/pay.php接收后执行:

// 1. 生成唯一订单号(格式:YYMMDDHHIISS + 6位随机数) $out_trade_no = date('ymdHis') . rand(100000, 999999); // 2. 组装支付请求参数(按网关文档要求排序) $params = [ 'mch_id' => $config['mch_id'], 'out_trade_no' => $out_trade_no, 'total_fee' => intval($price * 100), // 单位:分 'body' => '虚拟商品-会员月卡', 'notify_url' => $config['notify_url'], 'timestamp' => time(), ]; // 3. 按字典序拼接参数字符串并签名 ksort($params); $sign_str = http_build_query($params) . '&key=' . $config['api_key']; $params['sign'] = strtoupper(hash_hmac('sha256', $sign_str, $config['api_key'])); // 4. 调用网关统一下单接口 $response = curl_post('https://gateway.fuyou.com/v1/pay/unifiedorder', $params); $result = json_decode($response, true); if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') { // 重定向至网关支付页(如微信 H5、支付宝 WAP) header('Location: ' . $result['pay_info']['jump_url']); } else { die('支付初始化失败:' . $result['err_msg']); }

注意total_fee必须为整数分单位,body字段长度不能超过 32 字节(UTF-8),否则网关拒绝受理。jump_url是网关返回的跳转链接,包含预支付 ID,用户扫码/跳转后完成支付。

3.3 回调调试必备:log_callback.php日志埋点与常见失败代码含义

为定位验签失败问题,在pay_callback.php开头加入日志记录:

file_put_contents('/tmp/callback.log', "[" . date('Y-m-d H:i:s') . "] RAW: " . $raw_data . "\n", FILE_APPEND | LOCK_EX);

对照网关文档,高频错误码含义如下:

错误码含义排查方向
SIGN_ERROR签名不匹配检查api_key是否与网关后台一致;确认拼接字符串未多空格/换行;验证私钥格式
ORDER_NOT_EXIST订单不存在out_trade_no在本地数据库未找到;检查pay_callback.php中查询逻辑是否用错字段
TIMEOUT时间戳超时服务器时间与网关相差 >15 分钟;用ntpdate -u pool.ntp.org校时
REPEAT_NOTIFY重复回调pay_status未及时更新为 1;检查update_order_status()是否事务回滚

4. 自动发卡核心逻辑实现:卡密生成策略、库存原子扣减与多通道通知落地

4.1send_card.php的幂等性设计与并发安全控制

该脚本被pay_callback.php异步触发,必须保证同一订单只发货一次:

// send_card.php?id=12345 $order_id = (int)$_GET['id']; if (!$order_id) exit; // 1. 使用 SELECT ... FOR UPDATE 锁定订单行(InnoDB 行锁) $db->query("START TRANSACTION"); $order = $db->fetch("SELECT * FROM orders WHERE id = ? AND pay_status = 1 FOR UPDATE", [$order_id]); if (!$order) { $db->query("ROLLBACK"); exit; } // 2. 查询对应商品的卡密池(cards 表中 status=0 的未使用卡) $cards = $db->fetchAll("SELECT id, card_code FROM cards WHERE goods_id = ? AND status = 0 LIMIT ?", [$order['goods_id'], $order['quantity']]); if (count($cards) < $order['quantity']) { $db->query("ROLLBACK"); log_error("库存不足,订单 {$order_id} 发货失败"); exit; } // 3. 批量更新卡密状态为已使用(status=1) $card_ids = array_column($cards, 'id'); $db->query("UPDATE cards SET status = 1 WHERE id IN (" . implode(',', $card_ids) . ")"); // 4. 生成发货记录并更新订单状态 $card_codes = array_column($cards, 'card_code'); insert_ship_record($order_id, implode("\n", $card_codes)); $db->query("UPDATE orders SET pay_status = 2 WHERE id = ?", [$order_id]); // 2=已发货 $db->query("COMMIT");

提示SELECT ... FOR UPDATE是关键,避免高并发下超卖。若使用 MyISAM 引擎,必须改用LOCK TABLES全表锁,性能下降 5 倍以上。

4.2 卡密生成策略与防破解设计

卡密不采用简单 UUID 或随机字符串,而是组合式编码:

function generate_card_code($length = 16) { $prefix = substr(md5(time() . rand(1000,9999)), 0, 4); // 时间+随机前缀 $body = bin2hex(random_bytes(6)); // 12位十六进制 $checksum = substr(md5($prefix . $body), 0, 2); // 2位校验码 return strtoupper($prefix . $body . $checksum); } // 示例:A1B2c3d4e5f6g7h8

注意bin2hex(random_bytes())md5(rand())更安全,避免伪随机数碰撞。校验码用于前端输入时快速过滤无效卡密,减少数据库查询压力。

4.3 多通道通知实现:邮件、短信、站内信的配置与触发

通知逻辑封装在notify_user()函数中,根据后台配置动态启用:

function notify_user($user_id, $card_codes, $order_id) { $user = get_user_by_id($user_id); // 邮件通知(需配置 SMTP) if (CONFIG_EMAIL_ENABLED) { send_mail($user['email'], "您的虚拟商品已发货", "订单号:{$order_id}\n卡密列表:\n{$card_codes}"); } // 短信通知(调用第三方短信网关 API) if (CONFIG_SMS_ENABLED && $user['phone']) { $sms_content = "【平台名】您的订单{$order_id}已发货,卡密:{$card_codes}"; call_sms_api($user['phone'], $sms_content); } // 站内信(写入 messages 表) insert_message($user_id, "订单{$order_id}发货成功", "卡密已生成,可在「我的订单」查看"); }

提示:短信网关调用需添加失败重试机制(最多 3 次),并在messages表中增加notify_status字段标记发送状态,便于人工补发。

5. 生产环境关键优化与排错指南:MySQL 死锁规避、PHP-FPM 超时设置与支付通道降级策略

5.1 MySQL 死锁高频场景与SHOW ENGINE INNODB STATUS解析

发卡过程中最常见死锁发生在orderscards表交叉更新:

------------------------ LATEST DETECTED DEADLOCK ------------------------ 2023-10-05 14:22:31 0x7f8b1c012700 *** (1) TRANSACTION: TRANSACTION 123456, ACTIVE 0.001 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 1024 n bits 72 index PRIMARY of table `db`.`cards` trx id 123456 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 123457, ACTIVE 0.002 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 123 page no 1024 n bits 72 index PRIMARY of table `db`.`cards` trx id 123457 lock_mode X locks rec but not gap

根因:两个事务按不同顺序锁定cards表的主键行。解决方案:强制按id ASC顺序获取卡密:

-- 错误:无序 LIMIT SELECT id, card_code FROM cards WHERE goods_id = ? AND status = 0 LIMIT ?; -- 正确:显式 ORDER BY id SELECT id, card_code FROM cards WHERE goods_id = ? AND status = 0 ORDER BY id ASC LIMIT ?;

5.2 PHP-FPM 关键参数调优:避免支付回调超时中断

默认request_terminate_timeout=0(不限制),但网关要求回调在 5 秒内返回。需在www.conf中设置:

; 防止 send_card.php 长时间执行阻塞回调 request_terminate_timeout = 5s ; 允许回调脚本单独延长超时(仅限 pay_callback.php) php_admin_value[request_terminate_timeout] = 0 ; 但实际仍需在 pay_callback.php 中主动 exit,避免依赖超时

同时,send_card.php必须使用ignore_user_abort(true)+set_time_limit(0)确保后台发货不被中断:

ignore_user_abort(true); set_time_limit(0); // ...发货逻辑

5.3 支付通道降级策略:当主通道不可用时自动切换备用网关

/config/config.php中定义多通道配置:

'pay_gateways' => [ 'primary' => ['name' => 'fuyou', 'weight' => 80], 'backup1' => ['name' => 'yee', 'weight' => 15], 'backup2' => ['name' => 'tenpay_personal', 'weight' => 5], ],

/api/pay.php中按权重随机选取通道:

$gateways = CONFIG['pay_gateways']; $rand = rand(1, 100); $selected = 'primary'; foreach ($gateways as $key => $gw) { if ($rand <= $gw['weight']) { $selected = $key; break; } $rand -= $gw['weight']; } $config['pay_gateway'] = $gateways[$selected]['name'];

注意:降级不解决根本问题,需配合监控——每小时统计各通道SIGN_ERROR错误率,超 5% 自动触发告警并临时禁用该通道。

5.4 最小化验证清单:上线前必须执行的 5 项实测动作

动作命令/操作预期结果失败处理
1. 数据库连接测试php -r "new mysqli('127.0.0.1','user','pass','db');echo 'OK';"输出 OK检查config/db.php中 host/port/username/password
2. 支付回调模拟curl -X POST http://localhost/api/pay_callback.php -d '{"mch_id":"MCH_123","out_trade_no":"231005123456789","sign":"ABC"}'返回 success 且订单状态变为 1查看/tmp/callback.log确认原始数据
3. 发卡脚本单测php send_card.php?id=123cards表中对应卡密 status=1,orders表 pay_status=2检查send_card.php中 SQL 语句是否语法正确
4. 库存扣减压测ab -n 100 -c 10 http://localhost/api/pay.php无超卖(卡密生成数 = 下单数)启用SELECT ... FOR UPDATE并确认事务隔离级别为 REPEATABLE-READ
5. 通知通道验证修改notify_user()send_mail()error_log()查看php_errors.log是否有卡密输出确认 SMTP 配置或短信网关 API Key 有效

验证通过后,将web.config(如使用 IIS)或.htaccess(Apache)部署到位,确保/pay/xxx路径能正确路由至pay_callback.php,即可开放用户访问。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询