☰
PHP聚合支付系统源码实战:代付下单、卡密核销与回调对账全流程
2026/9/30 2:53:24 网站建设 项目流程

简介:这套源码面向电商支付系统开发者与虚拟卡密店铺运营者,整合了淘宝天猫代付、京东油卡卡密及聚合支付三类模块,可支持卡密店铺的协议回调与自动发卡流程。资源包共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 会直接报错,这是血泪经验,别问我怎么知道的。

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

组件建议版本说明
PHP7.2 ~ 7.48.0+ 需改废弃函数
MySQL5.7 / 8.0注意utf8mb4排序规则
Nginx1.18+负责静态与伪静态
Redis5.0+订单锁与回调去重
Composer2.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里放合法签名参数,否则全被验签挡掉,压测没意义。

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

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

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

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

立即咨询