ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP聚合支付系统源码实战:代付下单、卡密核销与回调对账全流程

PHP聚合支付系统源码实战:代付下单、卡密核销与回调对账全流程 简介这套源码面向电商支付系统开发者与虚拟卡密店铺运营者整合了淘宝天猫代付、京东油卡卡密及聚合支付三类模块可支持卡密店铺的协议回调与自动发卡流程。资源包共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装第三方 SDKPHP 扩展必须开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_daifugateway上游网关https://up.example.com/paymerchant_id上游商户号M202401secret签名密钥存加密后的值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. 拼接成 kvkv $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 21// 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里放合法签名参数否则全被验签挡掉压测没意义。从那以后我每次上线这类系统都会强制走一遍「沙箱回调 → 并发压测 → 对账比对」三步少一步都不敢接真实流量。希望帮到你。本文还有配套的精品资源点击获取
返回列表