简介:这套源码面向电商支付系统开发者与虚拟卡密店铺运营者,整合了淘宝天猫代付、京东油卡卡密及聚合支付三类模块,可支持卡密店铺的协议回调与自动发卡流程。资源包共2000个文件,以1412个js脚本、209个html页面、117个css样式、111个json配置为主,另含131个md说明文档、2个sql建表文件及少量docx、pptx资料,压缩包约75.33MB,整体结构接近可直接部署的完整项目。需注意系统实际仅完成天猫代付与京东中石油模块,京东中石化、比心、快手小店等仅有名称占位;天猫模块附带ck软件与使用教程,中石油模块需自行研究调试。目前已有157人学习下载,适合具备一定后端与前端基础、希望快速搭建卡密代付与聚合支付平台的开发者参考,可借此理解协议回调、订单自动处理与多支付渠道接入的实现思路。

1. 代付与卡密聚合系统:一套源码能跑通哪些真实业务

电商大促那几天,最怕的不是订单量涨,而是支付环节掉链子。买家想用代付、想用油卡卡密结算,后台却要对接三四个渠道,每个渠道的签名规则、回调格式、对账口径都不一样。这套「淘宝天猫代付系统 / 京东油卡卡密系统 / 聚合支付系统源码」要解决的,就是把代付下单、卡密核销、聚合收单这三条线收进同一套后台,用统一订单模型和回调网关去对接上游。它适合谁?做电商周边工具的小团队、需要给客户搭一套代付中台的开发者、以及想研究聚合支付订单流转的 PHP 后端。源码本身是 PHP 技术栈,常见做法是 Nginx + PHP 7.x + MySQL 5.7 起步,前台下单、后台管理、异步回调三块分离。下面按「能解决什么 → 怎么部署 → 订单怎么流转 → 坑在哪 → 怎么验证」的顺序拆开讲,每一步都落到能抄的配置和代码。

2. 环境搭建与目录结构:把源码跑起来的第一公里

2.1 运行环境选型与依赖清单

这套源码是典型的 PHP 单体应用,别一上来就想着容器化,先把最朴素的 LEMP 跑通再说。选型理由很直接:代付和卡密业务对并发要求不算极端,瓶颈通常在渠道回调的同步处理上,PHP-FPM 配合 MySQL 足够撑住中小体量。常见做法是 PHP 7.4,因为部分老代码用了 each() 之类的函数,PHP 8 会直接报错,这是血泪经验,别问我怎么知道的。

依赖清单如下,装之前先核对版本:

组件 建议版本 说明
PHP 7.2 ~ 7.4 8.0+ 需改废弃函数
MySQL 5.7 / 8.0 注意 utf8mb4 排序规则
Nginx 1.18+ 负责静态与伪静态
Redis 5.0+ 订单锁与回调去重
Composer 2.x 装第三方 SDK

PHP 扩展必须开 pdo_mysql 、 curl 、 openssl 、 bcmath 、 redis 。 bcmath 容易被忽略,但金额计算一旦用浮点,对账时就会出现一分钱的玄学差异,后面排查能让你怀疑人生。

2.2 目录结构与关键文件定位

解压后先别急着改代码,把目录摸清楚。典型结构长这样:

project/
├── application/          # 业务逻辑
│   ├── admin/            # 后台管理模块
│   ├── api/              # 对外下单接口
│   ├── notify/           # 渠道异步回调入口
│   └── common/           # 公共函数与订单模型
├── config/
│   └── database.php      # 数据库与 Redis 配置
├── public/
│   └── index.php         # 唯一入口
├── runtime/              # 日志与缓存,需可写
└── sql/
    └── install.sql       # 建表脚本

notify 目录是整套系统的咽喉,所有渠道的回调都从这里进,订单状态机也在这里被推动。部署时先把 runtime 权限给足,否则日志写不进去,回调失败你连现场都看不到。

2.3 数据库初始化与配置落地

导入建表脚本,然后改配置。这一步别偷懒,配置写错后面全是坑:

# 导入数据库结构
mysql -uroot -p your_db < sql/install.sql

# 给运行时目录写权限
chmod -R 755 runtime
chown -R www:www runtime
// config/database.php 关键片段
return [
    'host'     => '127.0.0.1',
    'port'     => 3306,
    'user'     => 'db_user',
    'password' => 'db_pass',
    'dbname'   => 'pay_center',
    'charset'  => 'utf8mb4',
    // Redis 用于订单锁,防止重复回调
    'redis'    => [
        'host' => '127.0.0.1',
        'port' => 6379,
        'auth' => '',
    ],
];

参数说明: charset 必须是 utf8mb4 ,卡密里可能带特殊字符;Redis 的 auth 如果线上开了密码,这里留空会直接连不上,回调去重就失效,重复发货的风险随之而来。配置改完,访问 public/index.php 对应的域名,能看到登录页就算第一公里跑通了。

3. 代付下单与卡密核销:订单状态机怎么流转

3.1 代付下单接口的参数与签名

代付的核心是「我替买家把钱付给上游,上游回调告诉我成功」。下单接口一般放在 api 模块,接收商户订单号、金额、代付渠道、回调地址,然后做签名校验。签名规则通常是参数按字典序拼接后加盐做 MD5 或 HMAC-SHA256,具体看源码里的 common/Sign.php 。

// application/api/controller/Order.php 下单核心逻辑
public function create()
{
    $params = input('post.');
    // 1. 必填校验
    if (empty($params['out_trade_no']) || empty($params['amount'])) {
        return json(['code' => 400, 'msg' => '参数缺失']);
    }
    // 2. 金额统一转成分,避免浮点误差
    $amountFen = bcmul($params['amount'], 100);
    // 3. 验签,防止篡改
    if (!Sign::verify($params, $params['sign'])) {
        return json(['code' => 401, 'msg' => '签名错误']);
    }
    // 4. 落库,状态置为待支付
    $orderNo = OrderModel::createOrder($params['out_trade_no'], $amountFen);
    // 5. 请求上游代付渠道
    $result = PayChannel::dispatch($params['channel'], $orderNo, $amountFen);
    return json($result);
}

逻辑说明:金额用 bcmul 转成分是硬性要求,浮点直接参与运算迟早对不上账。验签放在落库之前,避免脏数据进库。 PayChannel::dispatch 是渠道分发器,根据 channel 参数路由到不同上游,这是聚合支付的关键抽象点。参数上, out_trade_no 是商户侧唯一号,必须做唯一索引,否则重复下单会生成两笔订单。

3.2 卡密核销的库存扣减与并发控制

京东油卡卡密这类业务,本质是「先扣库存,再发卡密,回调确认」。库存扣减必须用数据库行锁或 Redis 原子操作,否则大促时超卖是必然的。常见做法是用 SELECT ... FOR UPDATE 锁住卡密批次行,再取一条未使用的卡密。

// application/common/service/CardService.php 卡密核销
public function consume($batchId, $orderNo)
{
    Db::startTrans();
    try {
        // 行锁锁定批次,防止并发超卖
        $batch = Db::name('card_batch')
            ->where('id', $batchId)
            ->lock(true)
            ->find();
        if ($batch['stock'] <= 0) {
            throw new Exception('库存不足');
        }
        // 取一条未使用卡密并标记占用
        $card = Db::name('card_item')
            ->where('batch_id', $batchId)
            ->where('status', 0)
            ->find();
        Db::name('card_item')
            ->where('id', $card['id'])
            ->update(['status' => 1, 'order_no' => $orderNo]);
        // 扣减批次库存
        Db::name('card_batch')
            ->where('id', $batchId)
            ->setDec('stock', 1);
        Db::commit();
        return $card['card_no'];
    } catch (Exception $e) {
        Db::rollback();
        return false;
    }
}

逻辑说明: lock(true) 生成 FOR UPDATE ,把批次行锁住,同一时刻只有一个请求能扣库存。卡密状态用 0/1/2 表示未用、占用、已用,回调成功后再把 1 改成 2,这样即使发货中途失败也能回滚。参数上 batchId 对应卡密批次, orderNo 用于追溯。注意事务里不要做 HTTP 请求,否则锁持有时间过长,并发直接崩。

3.3 异步回调与订单状态推进

上游回调进来后,第一件事是验签,第二件事是幂等。幂等靠订单状态判断,已成功的订单直接返回 success ,不再重复处理。

// application/notify/controller/Index.php 回调处理
public function handle()
{
    $raw = file_get_contents('php://input');
    $data = json_decode($raw, true);
    // 1. 验签
    if (!Sign::verifyNotify($data)) {
        exit('sign error');
    }
    // 2. 幂等:已处理直接返回
    $order = Db::name('order')->where('order_no', $data['order_no'])->find();
    if ($order['status'] == 2) {
        exit('success');
    }
    // 3. 更新订单状态并触发发货
    Db::name('order')->where('order_no', $data['order_no'])
        ->update(['status' => 2, 'pay_time' => time()]);
    // 4. 卡密类订单确认卡密
    if ($order['type'] == 'card') {
        CardService::confirm($order['order_no']);
    }
    exit('success');
}

逻辑说明:回调入口必须返回上游约定的成功标识,否则上游会不断重推。幂等判断放在最前面,避免重复发货。 confirm 把卡密状态从占用改成已用。参数上 order_no 是内部订单号,和商户订单号要能互相映射,建议建一张映射表,别只靠一个字段硬扛。

4. 聚合支付渠道对接:签名、回调、对账三件套

4.1 多渠道配置的抽象与分发

聚合支付的难点不在单个渠道,而在渠道多了之后配置散落各处。合理做法是建一张 pay_channel 表,把渠道编码、网关地址、商户号、密钥、回调地址都存进去,代码里只认渠道编码。

字段 含义 示例
channel_code 渠道编码 jd_oil、tmall_daifu
gateway 上游网关 https://up.example.com/pay
merchant_id 上游商户号 M202401
secret 签名密钥 存加密后的值
notify_url 回调地址 https://your.com/notify

分发器根据 channel_code 查配置,再调用对应渠道类。新增渠道时只加配置和渠道类,不动主流程,这是聚合系统能扩展的前提。

4.2 签名与验签的统一实现

签名规则各渠道不同,但结构可以统一:排序、拼接、加盐、加密。把差异收敛到每个渠道类的 sign() 方法里。

// application/common/library/Sign.php
public static function make(array $params, string $secret): string
{
    // 1. 去掉签名本身和空值
    unset($params['sign']);
    $params = array_filter($params, function ($v) {
        return $v !== '' && $v !== null;
    });
    // 2. 按 key 字典序排序
    ksort($params);
    // 3. 拼接成 k=v&k=v
    $str = urldecode(http_build_query($params));
    // 4. 加盐做 HMAC-SHA256
    return hash_hmac('sha256', $str, $secret);
}

逻辑说明: http_build_query 默认会 urlencode,所以先 urldecode 还原,否则签名对不上。 array_filter 去掉空值,很多渠道要求空参数不参与签名。参数 secret 从渠道配置读取,不要硬编码在代码里。验签就是拿同样规则算一遍再比对,比对用 hash_equals 防时序攻击。

4.3 对账文件的生成与差异处理

对账是聚合系统最容易被忽视的一环。每天凌晨拉上游对账文件,和本地订单逐笔比对,差异单独落表。常见做法是写一个定时脚本,用 diff 思路处理。

# crontab 每天凌晨 2 点跑对账
0 2 * * * /usr/bin/php /www/project/script/reconcile.php >> /www/logs/reconcile.log 2>&1
// script/reconcile.php 核心比对
$local = Db::name('order')->where('pay_time', 'between', [$start, $end])->select();
$remote = parseUpstreamFile($filePath); // 解析上游对账文件
foreach ($local as $order) {
    if (!isset($remote[$order['order_no']])) {
        // 本地有、上游无:可能上游漏单
        Db::name('reconcile_diff')->insert([
            'order_no' => $order['order_no'],
            'type'     => 'local_only',
            'amount'   => $order['amount'],
        ]);
    }
}

逻辑说明:对账要处理三种差异——本地有上游无、上游有本地无、金额不一致。每种都落表并告警,人工介入。参数上时间区间用 pay_time 而不是创建时间,因为只有支付成功的订单才需要对账。差异表要保留原始金额,方便财务核对。

5. 避坑与排查:那些让回调静默失败的细节

5.1 回调地址被框架路由拦截

现象:上游显示回调成功,但本地订单一直是待支付。原因:回调地址走了带鉴权的路由,或者被伪静态规则重写到了别处。解决:把 notify 入口单独配 Nginx location,跳过框架的登录中间件,并在日志里打印原始请求体确认能收到。

5.2 金额浮点导致对账差一分

现象:订单金额 99.99,对账时上游是 9999 分,本地算出来 9998。原因: float 参与乘法后精度丢失。解决:所有金额入库前统一用 bcmul 转成分存整数,展示时再除 100,全链路禁止浮点运算。

5.3 Redis 锁未释放导致订单卡死

现象:某笔订单一直占用,后续同批次卡密取不出来。原因:加锁后程序异常退出,没走到释放逻辑。解决:给锁设过期时间,用 SET key value NX EX 30 ,并在 finally 里兜底删除,别只依赖业务代码正常执行。

5.4 卡密状态回滚不彻底

现象:发货失败后卡密仍是占用状态,库存对不上。原因:只改了订单状态,没回滚卡密。解决:把订单状态和卡密状态放在同一个事务里,失败一起回滚,或者写补偿脚本定时扫描超时占用。

5.5 上游回调重复推送触发重复发货

现象:同一订单发了两张卡密。原因:幂等判断用了缓存但缓存过期,或者判断和更新之间有并发窗口。解决:幂等判断和状态更新放在一个事务里,用数据库唯一约束兜底,别只信缓存。

6. 上线前的验证清单与一个压测技巧

部署完别急着接真实渠道,先用沙箱把主流程走通。验证顺序建议是:本地下单 → 模拟回调 → 卡密核销 → 对账比对。模拟回调可以用 curl 直接打 notify 入口,构造合法签名,看订单状态是否推进。

# 模拟上游回调,验证幂等与状态推进
curl -X POST https://your.com/notify/index \
  -H "Content-Type: application/json" \
  -d '{"order_no":"T20240101001","amount":"99.99","status":"success","sign":"xxxx"}'

压测环节有个技巧:别用真实渠道,把 PayChannel::dispatch 临时改成直接返回成功,然后用 ab 或 wrk 打下单接口,观察订单表和卡密表的锁竞争。我一般会重点看两个指标——下单接口 P99 延迟和卡密扣减的失败率。如果失败率随并发上升,八成是行锁粒度过大,考虑把批次拆小或改用 Redis 预扣库存。

# 200 并发、总共 2000 请求压下单接口
ab -n 2000 -c 200 -p order.json -T application/json https://your.com/api/order/create

参数说明: -c 是并发数,别一上来就拉满,先 50 再 200 逐步加,观察 MySQL 的 innodb_row_lock_waits 。 order.json 里放合法签名参数,否则全被验签挡掉,压测没意义。

从那以后我每次上线这类系统,都会强制走一遍「沙箱回调 → 并发压测 → 对账比对」三步,少一步都不敢接真实流量。希望帮到你。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

更多推荐