☰
PHP发卡网源码实战:自动发卡系统下单支付发货全链路
2026/10/27 13:35:42 网站建设 项目流程

简介:面向PHP与Web开发者的爱发发卡网自动发卡平台商业源码,适合搭建游戏点卡、会员激活码、虚拟货币等数字商品销售站点,可降低发卡业务初期的开发成本。压缩包共2000个文件、约32.4MB,以892个PHP程序文件为核心,配合HTML、CSS、JavaScript构建前后台页面,并用大量GIF、PNG、JPG图片完善界面素材,另含接口与安装相关文件。源码已预留支付宝、财付通、微信支付、QQ钱包及易宝、云支付等支付接入思路,覆盖商品展示、下单支付、自动发卡、订单查询与后台管理等环节,并涉及注入过滤、脚本攻击防护和交易数据处理。已有1676人学习下载,适合熟悉PHP的开发者二次定制支付方式、页面布局与性能,也便于创业者快速验证发卡平台业务。

1. 手里压着几百个激活码,客服一条条手动发,这不是生意,是苦力

虚拟商品这门生意最耗人的环节从来不是获客,而是发货。客户凌晨两点下单,你第二天早上才回一句“稍等,马上发”,这单基本就凉了。这套 PHP 发卡网源码解决的就是这一段:用户在前台选商品、扫码付款、页面当场把卡密吐出来,中间不需要人守着。它的定位很明确——一套能自己部署、自己改的自动发卡系统,带后台、带卡密管理、带支付对接,还带分站和代理分佣这类商业功能。适合两类人:一类是把数字商品已经卖起来、需要把发货环节自动化的开发者;另一类是 PHP 基础还在练手阶段,想拿一条完整的“下单—支付—发货”链路拆开看的新手。包解开就是一个完整站点,PHP 加 MySQL 就能跑起来。

2. 技术栈与运行骨架:这套发卡网源码由哪些文件撑起来

发卡系统看起来功能不少,剥掉前台模板和后台表单,本质上是三件事:把卡密锁住、把钱收进来、把卡密交出去。这套源码的骨架也是围绕这三件事长出来的,所以在动任何一行代码之前,先搞清楚它跑在什么环境上、请求从哪个文件进来、数据落在哪几张表里,比急着改首页样式有用得多。下面按“环境—目录—数据表—部署”的顺序走一遍,每一步都对应到具体文件。

2.1 环境依赖与扩展清单

这类 PHP 发卡平台常见做法是原生 PHP 加一层轻量路由,也有跑在 ThinkPHP 上的版本,两者在目录形态上差别不大,配置文件和业务代码的位置基本一致。环境上,PHP 7.2 以上都能跑,生产建议 7.4 或 8.0,太新的版本反而容易踩到旧代码里被移除的函数。MySQL 用 5.7 起步,字符集统一 utf8mb4——卡密里带 emoji 或者特殊符号的情况比想象中多,用 utf8 存进去就是一堆问号。

扩展至少要装这几个:pdo_mysql(数据库)、curl(调支付网关接口)、openssl(RSA 签名验签)、mbstring(商品名截断,中文按字符算)、gd(后台验证码和收款二维码)、bcmath(金额计算,浮点数算钱迟早出事)、fileinfo(上传商品图)。Redis 是可选项,不加也能跑,加了之后限频和缓存会舒服很多。

要注意的是虚拟主机环境。很多便宜主机默认禁用 proc_open、putenv 之类的函数,二维码生成库和某些支付 SDK 恰好依赖它们,表现就是后台能进、订单一付就白屏,日志里什么都没有。我一般会在部署前先扔一个 phpinfo 上去,确认 disable_functions 这一项是空的。

2.2 目录结构与请求入口

解压之后先别急着配站点,把目录扫一遍,心里有个地图,后面改代码才不会翻箱倒柜。典型的目录结构是这样:

card/ ├── index.php # 前台入口,所有前台请求都从这里进 ├── admin/ # 后台独立入口,便于单独加 IP 白名单 │ └── index.php ├── api/ # 支付回调、订单查询等机器调用入口 │ ├── notify_alipay.php │ ├── notify_wechat.php │ └── query.php ├── app/ │ ├── controller/ # 业务控制器:商品、订单、用户 │ ├── model/ # 数据访问 │ └── lib/ # 支付网关、发货、工具类 ├── config/ │ ├── database.php # 数据库连接 │ └── payment.php # 支付渠道参数 ├── template/ # 前台模板,分站可覆盖 ├── static/ # css / js / 图片 ├── runtime/ # 缓存与日志,必须可写 └── upload/ # 商品图、收款码,必须可写

请求链路很短:Nginx 把非静态请求全部重写到 index.php,index.php 加载 config 和 app 里的自动加载,根据 URL 参数找到 controller 里的方法,方法里查 model、渲染 template。后台和 api 是两套独立入口,这么设计的好处是回调接口可以单独关掉伪静态、单独加访问日志,出问题时排查范围小很多。

2.3 四张核心数据表,读懂它就读懂了业务

后台菜单看着有十几项,落到数据库上真正撑起业务的就是下面这几张,剩下的配置表、日志表都是配角。

表名作用关键字段容易踩的点
goods商品id, site_id, name, price, stock, deliver_type, statusprice 用 decimal(10,2),别用 float
cards卡密id, goods_id, secret, status, order_no, lock_timestatus:0 未售、1 锁定、2 已售
orders订单order_no, goods_id, card_id, site_id, agent_id, amount, trade_no, status, create_time, pay_timeorder_no 必须建唯一索引
users用户/分站/代理id, pid, role, rate, balance, domain, statusrole 区分主站、分站、代理
commission_log分佣流水order_no, agent_id, amount, rate, status一单一条,order_no 加唯一索引防重
withdraw提现申请id, user_id, amount, status, audit_time状态机:0 待审、1 通过、2 拒绝、3 已打款

这张表里最值得盯的是 orders 和 cards 的关系。订单里存了 card_id,意味着下单那一刻卡密就被指定了,这是“预锁”方案,不是“付款后再挑一张”。两种方案各有代价,后面拆链路的时候细说。

2.4 从解压到进后台的最小部署路径

先把站点跑起来,再谈改代码。下面这套命令在宝塔和纯命令行环境都能用,路径按自己的实际情况替换:

# 1. 解压到站点根目录 unzip card-platform.zip -d /www/wwwroot/card cd /www/wwwroot/card # 2. 权限:只有 runtime 和 upload 需要写,其余保持只读更安全 chown -R www:www /www/wwwroot/card chmod -R 755 /www/wwwroot/card chmod -R 775 runtime upload config # 3. 建库,字符集务必 utf8mb4 mysql -uroot -p -e "CREATE DATABASE card DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -uroot -p card < install.sql

数据库配置一般落在 config/database.php,几个字段的含义值得单独说:

<?php // config/database.php return [ 'host' => '127.0.0.1', 'port' => 3306, 'dbname' => 'card', 'username' => 'card_user', // 别用 root,单独建库用户并只授权这个库 'password' => '换成你自己的强密码', 'charset' => 'utf8mb4', 'timezone' => '+08:00', // 少了这行,订单时间和回调时间会差 8 小时 ];

Nginx 的伪静态是另一个高频翻车点,重点是把 api 和 admin 两个目录排除在 rewrite 之外,否则支付回调会被前台路由吃掉,表现就是“钱付了,订单一直待支付”:

server { listen 80; server_name card.example.com; root /www/wwwroot/card; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } # 后台和回调走真实文件,不参与重写 location ^~ /admin/ { } location ^~ /api/ { } location ~ \.php$ { fastcgi_pass unix:/run/php/php7.4-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }

配置完访问域名,能出前台首页就算通了。后台入口一般是 /admin/,装完之后第一件事不是加商品,是改默认管理员账号密码,并且给 admin 目录加一层 IP 限制或者 HTTP Basic 认证——发卡站被扫后台是日常,不是意外。

3. 主链路拆解:一笔订单从提交到卡密发出的完整代码路径

前台一个“立即购买”按钮,背后是四段逻辑:下单锁定、拉起支付、回调确认、发货出库。这四段里任何一段写松了,都会变成线上事故,最常见的两种是超卖(同一张卡密卖给了两个人)和漏单(钱到了订单还是待支付)。下面按顺序把关键代码拆开看,都是这类系统里最典型的写法,可以直接对照你手上的文件改。

3.1 下单:库存校验与卡密预锁

下单接口是整个系统最需要小心的地方,因为它同时面对两件事:并发和篡改。前端传来的只有 goods_id 和联系方式,价格、金额、卡密全部由服务端决定,这是底线。

<?php // app/controller/Order.php 下单方法(示意,按你项目里的实际方法名对照) $pdo->beginTransaction(); try { // 1) 行锁住商品,防止两个请求同时读到同一份库存 $stmt = $pdo->prepare("SELECT id, price, status FROM goods WHERE id = ? FOR UPDATE"); $stmt->execute([$goodsId]); $goods = $stmt->fetch(PDO::FETCH_ASSOC); if (!$goods || $goods['status'] != 1) { throw new Exception('商品已下架'); } // 2) 锁一张可用卡密:状态 0 表示未售 $stmt = $pdo->prepare( "SELECT id FROM cards WHERE goods_id = ? AND status = 0 ORDER BY id ASC LIMIT 1 FOR UPDATE" ); $stmt->execute([$goodsId]); $card = $stmt->fetch(PDO::FETCH_ASSOC); if (!$card) { throw new Exception('库存不足,请联系客服'); } // 3) 卡密置为锁定态,带上订单号,超时由定时任务回收 $orderNo = date('YmdHis') . mt_rand(100000, 999999); $pdo->prepare("UPDATE cards SET status = 1, order_no = ?, lock_time = ? WHERE id = ?") ->execute([$orderNo, time(), $card['id']]); // 4) 落订单,金额永远取服务端商品价 $pdo->prepare( "INSERT INTO orders (order_no, goods_id, card_id, site_id, agent_id, amount, status, create_time) VALUES (?, ?, ?, ?, ?, ?, 0, ?)" )->execute([$orderNo, $goodsId, $card['id'], $siteId, $agentId, $goods['price'], time()]); $pdo->commit(); } catch (Exception $e) { $pdo->rollBack(); exit(json_encode(['code' => 0, 'msg' => $e->getMessage()])); }

这段代码里最关键的是两个FOR UPDATE。没有它们,两个并发请求会在同一瞬间读到同一张 status=0 的卡密,然后各自 UPDATE 一次,第二次覆盖第一次,最后两个人拿到同一串卡密,客服那边就是一串投诉。行锁把并发串行化了,代价是同一商品的并发下单会略微排队,虚拟商品这个量级完全扛得住。

预锁方案还有个副作用:用户下单不付款,卡密就一直挂在锁定态。所以必须配一个定时任务,把 lock_time 超过 15 分钟、订单仍是待支付的卡密退回 status=0,否则库存会被慢慢“吃掉”。另一种做法是付款成功后才分配卡密,不用锁,但要求回调那一刻必须能抢到库存,热门商品会出现“付了钱没货”的尴尬。两种都有人用,我倾向于预锁加回收,用户体验更稳。

3.2 支付:把支付宝、微信、聚合支付收进一个接口

发卡系统很少只接一个支付渠道,后台通常要能同时配支付宝当面付、微信 Native、以及第三方聚合支付。如果每个渠道都写一套 if-else,代码很快就没法看了。合格的做法是抽一个网关接口出来:

<?php // app/lib/pay/PayGateway.php interface PayGateway { // 创建支付,返回 ['type' => 'qrcode'|'redirect', 'data' => '...'] public function create(array $order): array; // 回调验签,返回 bool public function verify(array $payload): bool; // 把各渠道字段归一化成统一结构 public function parse(array $payload): array; }

具体渠道实现里,最容易出错的是签名。以支付宝为例,参数拼接顺序、编码、空值过滤,少一步就是验签失败:

<?php // app/lib/pay/AlipayGateway.php class AlipayGateway implements PayGateway { private $config; public function __construct(array $config) { $this->config = $config; // app_id / private_key / public_key / notify_url } public function create(array $order): array { $biz = [ 'out_trade_no' => $order['order_no'], // 商户订单号,唯一 'total_amount' => $order['amount'], // 单位元,两位小数 'subject' => mb_substr($order['goods_name'], 0, 60), 'product_code' => 'FACE_TO_FACE_PAYMENT', ]; $params = $this->buildParams($biz); // 公共参数 + 业务参数 $params['sign'] = $this->rsaSign($this->sortAndJoin($params)); return ['type' => 'qrcode', 'data' => $this->post('alipay.trade.precreate', $params)]; } public function verify(array $payload): bool { $sign = $payload['sign'] ?? ''; unset($payload['sign'], $payload['sign_type']); // 验签前必须剔除这两个字段 return $this->rsaCheck($this->sortAndJoin($payload), $sign); } }

create()返回的是一张二维码的内容,前台拿到后渲染成图片让用户扫;parse()则把支付宝的 trade_no、微信的 transaction_id 统一成['order_no','trade_no','amount','paid_at']四个字段,回调处理逻辑就完全不用关心是哪个渠道来的。notify_url 一定要写成带 https 的独立路径,很多渠道只回调 https,配置错了就是收不到通知。

3.3 回调:幂等、金额校验与补单

支付回调是整套系统里最不能出问题的一段,因为它会被重复调用——渠道在没收到 success 应答时,会按 3s、10s、30s 的间隔反复推同一条通知。处理不当的后果是同一张订单发两次货,或者余额加两次。

<?php // api/notify_alipay.php require '../app/bootstrap.php'; $gateway = new AlipayGateway(config('payment.alipay')); if (!$gateway->verify($_POST)) { exit('fail'); // 验签不过直接拒,绝不写库 } $data = $gateway->parse($_POST); // order_no / trade_no / amount / paid_at $pdo->beginTransaction(); try { $stmt = $pdo->prepare("SELECT * FROM orders WHERE order_no = ? FOR UPDATE"); $stmt->execute([$data['order_no']]); $order = $stmt->fetch(PDO::FETCH_ASSOC); if (!$order) { $pdo->rollBack(); exit('fail'); } // 幂等核心:已经处理过的订单直接应答成功,不再走后面的逻辑 if ($order['status'] >= 1) { $pdo->commit(); exit('success'); } // 金额必须一致,不一致只记日志不出卡 if (bccomp((string)$data['amount'], (string)$order['amount'], 2) !== 0) { $pdo->rollBack(); file_put_contents('../runtime/pay_mismatch.log', json_encode($data) . PHP_EOL, FILE_APPEND); exit('fail'); } $pdo->prepare("UPDATE orders SET status = 1, trade_no = ?, pay_time = ? WHERE order_no = ? AND status = 0") ->execute([$data['trade_no'], $data['paid_at'], $data['order_no']]); $pdo->commit(); } catch (Throwable $e) { $pdo->rollBack(); exit('fail'); } deliver($data['order_no']); // 发货独立出去,失败可以重放 exit('success');

这里有三层保护。第一层是status >= 1直接返回 success,重复通知不会重复处理;第二层是 UPDATE 语句里带AND status = 0,即使有并发也只有一个请求能改成功;第三层是金额用 bcmath 逐位比较,超过两位小数直接判失败。success这个应答字符串是渠道规定的,写成ok或者 JSON 都会被当成失败,然后被无限重推。

补单是必须配的。回调链路里任何一环超时(数据库锁等待、down 了一瞬间),订单就会停在待支付。常规做法是写一个每分钟跑一次的脚本,把创建超过 2 分钟、状态还是 0 的订单拿去渠道查单,查到已支付就调用同一个deliver()。

3.4 发货:条件更新与通知

发货单独抽成一个函数,好处是所有入口——正常回调、手动补单、后台点“重新发货”——都走同一段代码,出问题只需要看一个地方。

<?php // app/lib/deliver.php function deliver(string $orderNo): bool { global $pdo; $stmt = $pdo->prepare("SELECT * FROM orders WHERE order_no = ? AND status = 1"); $stmt->execute([$orderNo]); $order = $stmt->fetch(PDO::FETCH_ASSOC); if (!$order) return false; // 订单不存在或状态不对 // 条件更新:只有仍处锁定态的卡密才能被这次请求吃掉 $upd = $pdo->prepare("UPDATE cards SET status = 2, use_time = ? WHERE id = ? AND status = 1"); $upd->execute([time(), $order['card_id']]); if ($upd->rowCount() !== 1) { // 卡密已被回收或被他单占用,记日志交给补单重新分配 file_put_contents('../runtime/deliver_fail.log', $orderNo . PHP_EOL, FILE_APPEND); return false; } $pdo->prepare("UPDATE orders SET status = 2, deliver_time = ? WHERE order_no = ? AND status = 1") ->execute([time(), $orderNo]); return true; }

rowCount() !== 1这一行是整个函数的价值所在。它把“这张卡密到底是不是我的”这个问题交给了数据库来判断,而不是先 SELECT 再 if。订单状态码建议统一成:0 待支付、1 已支付待发货、2 已完成、3 已退款、4 已关闭,前后台和补单脚本都按这套码走,别在某个页面里自己发明一个 -1。

4. 后台商业模块:商品、卡密、分站代理与结算提现

前台决定用户买不买,后台决定这门生意能不能规模化。这套源码之所以叫商业版,价值基本都压在后台这几个模块上:卡密能不能批量管、分站能不能自己带域名、代理的佣金算得准不准、提现流程有没有留痕。下面按运营实际会碰到的顺序拆。

4.1 商品与卡密:批量导入的三个校验

卡密导入是运营每天都要用的功能,做糙了天天有人来找。上传一个 txt 或 csv,一行一张卡,服务端至少要过三道校验:去重(同一商品下 secret 唯一)、去空(去掉行尾空格和 BOM 头)、格式(长度和字符集校验,避免把整段说明文字当卡密导进去)。

<?php // 卡密批量导入(示意) $lines = preg_split('/\r\n|\r|\n/', $raw); $seen = []; $ok = 0; $pdo->beginTransaction(); $stmt = $pdo->prepare("INSERT IGNORE INTO cards (goods_id, secret, status, create_time) VALUES (?, ?, 0, ?)"); foreach ($lines as $line) { $secret = trim($line); if ($secret === '' || isset($seen[$secret])) continue; // 空行与文件内重复 $seen[$secret] = 1; $stmt->execute([$goodsId, $secret, time()]); $ok += $stmt->rowCount(); // 被唯一索引挡下的不计入 } $pdo->commit();

INSERT IGNORE配合 cards 表上(goods_id, secret)的唯一索引,能把“库里已存在”和“文件内重复”两种情况一次挡掉,最后$ok就是真实入库条数。导入完记得同步一下商品的 stock 字段——很多模板的库存是独立存的,不同步就会出现前台显示有货、下单提示没货。

4.2 分站与代理:site_id 和 agent_id 怎么贯穿全链路

分站是这类系统的核心卖点:主站开一个后台账号,绑定独立域名,分站有自己的模板、自己的商品、自己的收款配置。技术上靠一个 site_id 贯穿到底——下单时从域名反查站点,把 site_id 写进订单;回调时不去读 cookie,只认订单里落库的 site_id。这一点很关键,回调是服务器对服务器,用户的 cookie 早就没了,靠 session 判断归属必然出错。

代理是另一条线,走的是人而不是域名,靠 agent_id 和一个分佣比例。比例可以按全局配,也可以按商品单独覆盖,取的时候按“商品级优先、全局兜底”的顺序。佣金不是在回调里直接加余额就完事,要先落流水再更新余额,两条语句放同一个事务里:

<?php // 回调成功后按订单里落库的 agent_id / site_id 分佣 $rate = $order['agent_id'] ? getAgentRate($order['agent_id'], $order['goods_id']) : '0'; $commission = bcmul((string)$order['amount'], (string)$rate, 2); if (bccomp($commission, '0.00', 2) === 1) { $pdo->prepare( "INSERT INTO commission_log (order_no, agent_id, site_id, amount, rate, status, create_time) VALUES (?, ?, ?, ?, ?, 0, ?)" )->execute([$order['order_no'], $order['agent_id'], $order['site_id'], $commission, $rate, time()]); $pdo->prepare("UPDATE users SET balance = balance + ? WHERE id = ?") ->execute([$commission, $order['agent_id']]); }

rate建议存成字符串形式的比例(比如 0.15),金额一律用 bcmul 算,别用$amount * $rate直接乘——浮点数乘出来的 0.30000000000000004 会在对账时把人逼疯。commission_log 上给 order_no 加唯一索引,重复回调就写不进第二条。

4.3 结算与提现流水

代理的钱在 balance 里,能不能提、提多少、什么时候打款,是运营最容易和人对账的地方。规范做法是把 balance 拆成两个概念:可用余额和冻结余额。申请提现时从可用余额扣、加到冻结余额,审核通过打款后再把冻结余额清零,审核拒绝则原路退回可用余额。整个过程每一步都写一条 withdraw 记录的状态变更,别只更新主表。

提现状态机就四个值:0 待审核、1 已通过待打款、2 已拒绝、3 已完成。任何一步操作都要带AND status = 上一个状态的条件,防止两个管理员同时点审核点出两笔款。金额下限、手续费、每日次数这些参数放配置表,运营改起来不用动代码。

4.4 风控:限频、金额校验与库存预警

发卡站被刷是常态,攻击成本极低——写个脚本循环下单不付款,就能把卡密全锁死,别人一单都下不了。最低成本的防护是下单限频:

<?php // 单 IP 60 秒内最多创建 5 笔未支付订单 $key = 'order:limit:' . $ip; $count = $redis->incr($key); if ($count === 1) { $redis->expire($key, 60); } if ($count > 5) { exit(json_encode(['code' => 0, 'msg' => '操作过于频繁,请稍后再试'])); }

没有 Redis 就用数据库计数表,效果差一点但能用。除了限频,还要盯三件事:金额篡改(服务端取价,前端传的一律不信)、黑名单(把恶意 IP 和 uid 存表,下单前查一次)、库存预警(某商品剩余卡密低于阈值就给管理员发通知,别等卖空了才发现)。这三条加起来,能挡掉九成以上的低级骚扰。

5. 避坑与常见问题排查:上线前必须过的 5 道坎

这套源码在本地跑通和在线上稳定跑,中间隔着的不是功能,是一堆环境差异。下面这几条都是我实际碰到过的,按“现象—原因—解决”记下来,部署完照着过一遍,能省掉大半夜的排障。

5.1 部署与访问类问题

现象:后台登录成功,点任何菜单又跳回登录页。原因通常是 session 写不进去,或者 cookie 域不一致。runtime 目录权限没给够是第一种;站点同时能用带 www 和不带 www 的域名访问是第二种,登录时 cookie 写在了一个域上,跳转后另一个域读不到。解决办法:确认session.save_path指向的目录对 www 用户可写,并在配置里显式指定session.cookie_domain,然后把不带 www 的域名 301 到带 www 的,只保留一个入口。

现象:前台首页正常,商品详情页 404。伪静态规则没生效,或者规则写在了错误的 location 块里。Nginx 里try_files $uri $uri/ /index.php?$query_string;必须放在location /内部,而不是 server 顶层。另外检查一下是不是把/api/和/admin/也一起重写了,那样回调会直接失效。

现象:上传商品图提示成功,但图片裂开。两种情况:upload 目录没有写权限,文件根本没落地;或者 Nginx 没有为 upload 目录配置静态资源处理,请求被重写到 index.php 了。查一下目录里有没有真实文件,有文件就是配置问题,加一个location ^~ /upload/ { }即可。

5.2 支付与发货类问题

现象:用户扫码付了钱,订单状态一直是待支付,客服手动补单才行。概率最高的原因是回调地址不可达:站点没配 https、notify_url 写成了内网地址、或者防火墙拦了渠道的回调 IP。排查顺序是先去渠道商户后台看通知记录,那里会明确写“通知失败”还是“返回内容不正确”。如果显示通知成功但订单没变,就是代码问题,多半是验签失败被exit('fail')挡在门外,这时候临时在验签前打一条原始 POST 到日志,一目了然。

现象:同一张卡密发给了两个订单,或者订单完成了但卡密还是锁定态。前者是下单时少了FOR UPDATE或者事务隔离级别不对,后者是发货时用了“先 SELECT 再 UPDATE”而不是条件更新。还有一个隐蔽原因:卡密回收的定时任务跑得太激进,把已经付款但还没发货的订单的卡密回收了。回收条件里必须排除“订单状态 >= 1”的卡密,只看 status=1 且订单仍是待支付的那些。

5.3 数据与分佣类问题

现象:订单列表里的时间和实际付款时间差 8 小时。PHP 的date.timezone没设,或者 MySQL 连接后没执行SET time_zone = '+08:00'。两边都要设,只设一边的话,PHP 写进去的时间是对的,MySQL 的 NOW() 又差 8 小时,对账时看着像灵异事件。我一般会在 bootstrap 里固定写死date_default_timezone_set('Asia/Shanghai'),在 PDO 连上之后立刻执行一次时区设置。

现象:分站的订单跑到了主站,代理佣金算成 0。根因是归属标识没落库。下单时如果只把 site_id 存在 cookie 或者 session 里,回调那一刻这些上下文全没了,只能按主站处理。正确做法是下单时就把 site_id、agent_id 写进 orders 表,回调只读订单,不读任何会话状态。改完之后拿两个不同域名的分站各下一单,去数据库里核对 orders.site_id 是不是对得上,这一步别省。

6. 二次开发:把通用发卡平台改成自己的数字商品发货中台

这套源码的通用性来自“商品—卡密”这个模型,局限也来自它。真实业务里要发的东西远不止一串卡密:有的是一段固定文本(激活说明),有的是要去上游接口实时拿货,有的是一个下载链接。硬塞进 cards 表只会越改越乱,我的做法是在发货这一层加一个策略接口,把“发什么”和“怎么发”拆开:

<?php // app/lib/deliver/ Deliver.php interface Deliver { // 返回 ['ok' => bool, 'content' => '给用户看的内容', 'msg' => '失败原因'] public function handle(array $order): array; } // 卡密型:从 cards 表取 class CardDeliver implements Deliver { /* 就是前面那个 deliver() 的逻辑 */ } // 固定文本型:内容存在 goods.deliver_content 里,直接返回 class TextDeliver implements Deliver { /* ... */ } // 接口型:带上订单号去调上游,成功才写 orders.status = 2 class ApiDeliver implements Deliver { /* ... */ }

商品表加一个 deliver_type 字段做分发,deliver()里根据类型实例化对应实现。这么改的好处是新增一种发货方式只需要加一个类,主链路一行不动。

改造清单我一般按这个顺序推进:

改造点涉及文件风险提示
发货策略化app/lib/deliver/*、goods 表加 deliver_type老数据要补默认值,否则查不到实现类
支付网关扩展app/lib/pay/*、config/payment.php新渠道的回调路径要单独放行
下游通知新增 app/lib/notify.php,发货成功后异步推送必须做成失败重试,不能同步阻塞回调
发货日志新增 deliver_log 表,记录每次调用入参出参排查线上问题全靠它,字段别省

验证方式很简单,也很笨:准备一个测试商品,把四种发货类型各配一遍,用渠道沙箱或者后台的“模拟支付”按钮逐笔走完,然后去数据库核对 orders 的 status、cards 的 status、deliver_log 的条数三者是否一致。再手动把一张卡的 status 改成 0,模拟卡密被回收的极端情况,看补单脚本能不能把它救回来。

有个习惯是踩坑踩出来的:任何一次发货动作,无论成功失败,都必须往 deliver_log 写一条带订单号和耗时的记录。早期我省过这张表,结果有次上游接口超时,订单卡在已支付待发货,日志里什么都没有,只能一张张对着支付流水手工补。从那以后我每次改发货逻辑,都强制走一遍“正常单、重复回调、卡密被回收、上游超时”这四个用例,跑通了才敢上线。希望帮到你。

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

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

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

立即咨询