☰
代付系统源码解析:从代收代付到五合一架构实战
2026/10/7 4:43:03 网站建设 项目流程

简介:这是面向代付/聚合支付场景的五合一代付系统前端源码包,覆盖美团、携程、京东、拼多多、滴滴等多平台模板,适合具备前端与支付业务基础的开发者二次开发,或用于学习多商户代付页面实现。压缩包共118个文件,整体约1.34MB,核心代码以31个js、24个tsx及4个ts为主,对应React/TypeScript组件与业务逻辑;另含8个html、7个json、5个svg、css等界面资源,以及安装搭建教程docx、环境配置env、SQL脚本和PEM证书等文件,便于本地部署与接口联调;这些配置与脚本覆盖环境初始化、数据导入和证书校验,部署时尤其实用。当前已有645人学习下载。通过源码可快速了解五合一模板的页面结构、多端路由与主题切换方式,配合教程文档可理清代付提单流程;对于需要搭建同类型演示站或研究主流外卖平台代付交互的开发者,是一份结构清晰、可直接对照查阅的实用参考。

1. 代付系统源码不是黑匣子:这个zip里到底在做什么生意

拿到「美团代付系统源码 支付系统代收代付 五合一代付系统源码 滴滴代付多模版.zip」这个压缩包,先用五分钟看一眼目录结构,没必要急着解压去欣赏页面。标题里的核心词不是“美团”也不是“滴滴”,而是“代付系统”——平台替商户把钱打给指定的个人或结算账户。美团代付、滴滴代付多模板,通常只是两套演示皮肤加对应的业务参数,真正值钱的是那一层能把代付单发到上游渠道、收回调、做对账的后端代码。适合谁?做过支付但没碰过代付的 PHP/Java 后端、准备做聚合结算产品的小团队,以及总把代收和代付混为一谈的测试同学。读完你能分清代收代付的差异,也知道该在哪个文件里改配置,而不是只在模板上换颜色。

2. 代收代付与五合一:先把业务模型拆开

2.1 代收与代付在资金流上不是镜像

代收和代付,一进一出,但技术上是两套完全不同的资金流模型。代收是“先拿到用户授权再扣款”,风险在扣款前:要素验证、短信验证码、签约协议、扣款失败后的自动重试。代付是“主动出款”,风险在扣款后:钱一旦出去,回来就难了。很多源码翻车,就是因为把代收的幂等策略原样套在代付上,单号重复、金额校验不严,结果一笔失败单被重试打给真实用户。

另一个容易被忽略的差别是状态机。代收的状态一般是“待扣款 -> 扣款中 -> 已扣款”,失败可以冲正;代付的状态则是“待出款 -> 处理中 -> 代付成功 / 代付失败”。两个“处理中”的含义完全不同,代收扣款失败可以自动冲正,代付进入中间态后一旦真实资金已划出,就不能简单撤销。所以代付系统的数据库表、异步回调、查单补偿逻辑都要单独设计,不要和代收共用一张订单表。

在这个标题的源码包里,如果看到 controller 里把代收和代付接口放在同一个类里,先别急着夸代码优雅,要看两个接口是否真的走了独自的校验、幂等和回调处理链路。共用一张表但用biz_type区分,常见但风险不小;一旦渠道回调顺序乱掉,代付单被代收逻辑处理,资金方向就反了。

2.2 五合一代付系统里“五”指什么

“五合一”在不同源码包里的定义很乱。最干净的理解是五个代付渠道适配器并行存在,统一接在一个抽象层后面。我见过最常见的组合是:银行卡代付、支付宝余额代付、微信商户代付、企业内部钱包余额划拨,外加一个模拟测试网关。为什么测试网关也算一个?因为真实代付渠道联调成本高,工作日窗口、金额上限、同名限制一堆,Mock 网关能让开发者先跑通状态机和回调,再把真实渠道参数换进去。

序号常见代付渠道接口特点到账时效回调方式
1银行卡代付需要收款卡号、姓名、部分场景校验身份证后四位两小时或 T+1异步回调 + 日终对账文件
2支付宝余额代付按账号或用户 ID 打款实时或几分钟异步回调
3微信商户代付按 openid 或转账凭证打款实时或半小时异步回调
4内部钱包余额划拨系统内部账户间转账即时无回调,靠主动查询
5Mock 测试网关本地 HTTP 服务即时可配置延迟和回调

“五合一”还有另一种解释,指五个业务模板:美团模板、滴滴模板、打车模板、外卖模板、通用模板。这种包的重点不在渠道适配,而在商户端 UI。判断标准很简单:解压后看有没有channel/或payment/adapter目录。有,五合一指的是渠道;没有,五合一多半只是五套页面皮肤,代付逻辑仍然只有一家渠道在跑。这方面看走眼,后面接第二家渠道时才会发现所有逻辑全是if ($channel == 'ali')写死的。

五合一对开发者的真正价值是检验抽象层是否合格。一个合格的五合一系统,新增第六个渠道时,只需要新增一个 adapter 类、改一下路由注册,而不是在 Controller 里再加一个elseif。后面第 4 章会直接把这套 adapter 结构写出来。

2.3 美团/滴滴这类多模板角色在哪一层

美团和滴滴是业务场景模板,不在支付核心链路里。美团模板常见的演示场景是“企业给员工统一代付外卖餐费”,滴滴模板是“企业给司机代付加油费或报销款”。对代付系统来说,模板到底影响什么?影响三样东西:后台界面风格、商户端默认渠道、代付单展示文案。

实际代码里,这些通常靠merchant_id -> template_id映射完成,模板本身只是视图目录和按钮文案。真正干活的代付下单、回调、对账接口,所有模板共用同一套。也就是说,你换页面皮肤不会影响资金流,但如果你在源码包里看到template/meituan/controllers/这种目录,里面真的写了独立的支付逻辑,就要警惕了:每个模板一套逻辑,最终很难维护。

拿到 zip 后建议先做一次轻量检查,别急着解压覆盖:

unzip -l "美团代付系统源码*.zip" | awk '{print $4}' | grep -E '(template|channel|static)' | head -80

这一步能很快看清 template 和 channel 是不是双层结构。如果channel目录里是alipay、wechat、bank、wallet、mock这类子目录,说明渠道适配层是真实存在的;如果channel只有一堆空目录,UI 模板再多也只是演示包。还有个常见细节:这类 zip 经常附带微信小程序前端工程,当作模板的一部分交付。如果你只改了小程序里的域名就去连后台,回调地址、商户标识、渠道白名单大概率全是旧值,联调时会花掉大量时间。

3. 用 PHP 内建服务器跑通代付后台的最小命令

3.1 解压前看三个地方

第一,看压缩包内部路径有没有恶意穿越。代付系统源码这种 zip 在灰色渠道里流传时,经常被夹带后门,路径穿越和文件覆盖是很常见的暗坑。

unzip -l "美团代付系统源码*.zip" | awk '{print $4}' | grep -E '(\.\./|\.phtml|\.php\.|eval\()' | head -30

看到../开头或者.php.后缀的条目,不一定就是后门,但必须人工核对。路径穿越意味着解压时会写出到指定目录之外;扩展名伪装是为了绕过服务器对.php的执行限制。

第二,检查 zip 是否有加密标志。unzip -l输出列表的最后一列如果是[encrypted],说明整个包是带密码的。带密码本身不代表有问题,但很多支付源码包的密码写在注释或 readme 里,解压前先把密码找出来,别解到一半卡住。

第三,确认解压后是否有一个独立根目录。很多代付系统源码解压后把application、public、config全散在同级目录,直接覆盖当前文件夹的config.php。常见做法是强行新建目录再解压:

mkdir -p /data/www/pay_system cd /data/www/pay_system unzip /tmp/美团代付系统源码*.zip ls -la

这里ls -la看是不是只有根目录。散落文件的压缩包,不管代码多好都建议先整理目录结构再启动,否则后面改配置时不知道哪个文件被旧包覆盖了。

3.2 运行环境选型

这类源码最常见的是 PHP 系,ThinkPHP 和 Laravel 各占一半,也有少量 Java Spring Boot 版本。先确认包里有没有composer.json,如果有,它是 PHP 项目;如果没有但看到pom.xml,就是 Java。PHP 项目本地验证最省事的是内建服务器,但要注意:内建服务器是单进程的,能验证接口逻辑,验证不了并发和回调竞争。

环境版本建议这样给:

组件建议版本说明
PHP7.4 / 8.0 / 8.1不要直接用 8.3,老源码常有 deprecated 报错
MySQL5.7 / 8.0必须开 utf8mb4
Redis6.x用于幂等锁,2.x 也能跑但没必要
Composer2.x安装依赖用

为什么必须要有 Redis?代付防重复锁和回调去重高度依赖 Redis 的原子操作。如果没有 Redis,用 MySQL 唯一索引能挡住一部分重复,但并发下两个请求同时在数据库里 “查状态为空” 后继续执行,会真实重复调用渠道代付接口,这不是概率问题,是必然问题。

3.3 配置数据库和处理金额字段

先建库,然后导入源码自带的 SQL 或迁移文件:

mysql -uroot -p -e "CREATE DATABASE pay_system DEFAULT CHARSET utf8mb4;" mysql -uroot -p pay_system < install.sql

如果源码包里没有现成的 SQL 文件,或者表结构里有float类型的金额字段,建议直接改成下面这个核心表,这是代付系统最要紧的一张表:

CREATE TABLE `pay_order` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(64) NOT NULL COMMENT '商户代付单号', `platform_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '平台流水号', `user_id` INT UNSIGNED NOT NULL COMMENT '发起代付的商户/用户ID', `amount_fen` BIGINT UNSIGNED NOT NULL COMMENT '代付金额,单位分', `fee_fen` BIGINT NOT NULL DEFAULT 0 COMMENT '预计算手续费,单位分', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0初始化 1处理中 2成功 3失败 4待复核', `channel_id` TINYINT NOT NULL DEFAULT 0 COMMENT '渠道ID', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_status` (`user_id`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代付订单表';

金额用BIGINT表示分,禁止用float或double。order_no是商户传过来的单号,必须有唯一键,这是挡重单的第一层。platform_no是渠道返回的平台流水号,在渠道受理后才生成,所以不能设唯一键——多个渠道可能各自维护一套流水号体系,设了唯一键反而把自己卡死。

接下来改环境配置,常见.env长这样:

APP_ENV=dev APP_DEBUG=true DB_CONN=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=pay_system DB_USERNAME=root DB_PASSWORD=test REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PREFIX=pay_system: CHANNEL_ENABLE=1,2,3,4,5 PAY_CALLBACK_URL=http://192.168.31.10:8000/api/callback

这里最坑的是PAY_CALLBACK_URL。本地联调时回调地址要填局域网 IP,别写127.0.0.1。渠道回调是从另一个进程发起请求,指向127.0.0.1会打到它自己,不是打到你正在调试的电脑。很多新手在这个字段上能卡掉半天。

3.4 启动命令与健康检查

依赖安装和启动,以 Laravel 风格为例:

cd /data/www/pay_system composer install --no-dev php artisan migrate --seed php -S 0.0.0.0:8000 -t public public/index.php

如果包里是 ThinkPHP 结构,入口命令换成:

php think run -H 0.0.0.0 -p 8000

启动后用 curl 打健康检查:

curl -s http://127.0.0.1:8000/api/ping

正常会返回 JSON,比如{"code":0,"data":"pong"}。如果返回 HTML 错误页,先看storage/logs下的日志,通常是数据库连不上或.env没读对。这里有一点要提醒:内建服务器只能证明“能启动”,不能证明“并发正确”。要验证回调幂等,至少要用 Mock 渠道压 20 个并发请求,后面第 6 章会讲具体清单。

4. 代付主流程:下单、回调、对账三个关键节点

4.1 商户下单与签名校验

代付接口通常叫pay/issue,请求参数至少有merchant_id、order_no、amount_fen、settle_account、sign。这里用settle_account而不是bank_no,是因为五合一渠道里,结算账户可能是银行卡号,也可能是支付宝账号或微信 openid。统一放在一个字段里,由渠道适配层决定怎么用,而不是在 Controller 里写死。

签名算法常见的是 SHA256 with RSA,先把参数名按 ASCII 升序排序,拼接成键值对,再用商户私钥签名,服务端用商户公钥验签。

// app/Http/Controllers/Api/PayController.php public function issue(Request $request) { // 验签前只取白名单参数,避免 sign 和 extra 混入原文 $params = $request->only(['merchant_id', 'order_no', 'amount_fen', 'settle_account']); $sign = (string) $request->input('sign', ''); // 按参数名排序后拼接 $payload = collect($params) ->filter(fn($v) => $v !== '') ->keys() ->sort() ->map(fn($k) => $k . '=' . $params[$k]) ->implode('&'); if (!openssl_verify($payload, base64_decode($sign), $merchant->public_key, OPENSSL_ALGO_SHA256)) { return ['code' => 40001, 'msg' => '签名校验失败']; } // Redis 锁做单号幂等,防并发重复下单 $lockName = "pay_lock:{$params['merchant_id']}:{$params['order_no']}"; if (!Redis::set($lockName, '1', 'EX', 5, 'NX')) { return ['code' => 40002, 'msg' => '重复下单请求']; } $order = PayOrder::create([ 'order_no' => $params['order_no'], 'user_id' => $params['merchant_id'], 'amount_fen' => intval($params['amount_fen']), 'status' => 0, 'channel_id' => $this->routeChannel($params['merchant_id'], intval($params['amount_fen'])), ]); // 异步处理,立刻向商户返回受理结果 dispatch(new ProcessPayout($order->id)); return ['code' => 0, 'data' => ['order_no' => $order->order_no, 'status' => 'pending']]; }

逻辑说明:白名单参数过滤很重要,sign和额外的extra字段不能进验签原文,否则不同调用方拼出来参数顺序不一样,永远验不过。Redis::set的NX是原子操作,同一商户同一单号 5 秒内只有第一个请求能拿到锁。最后把耗时渠道调用丢进队列,接口可以立刻响应,商户端不会因为渠道慢而大量超时。

如果源码包里没有队列环境,退而求其次的做法是同步调用渠道,但 HTTP 超时时间至少要设 8 秒,超过 8 秒要标记为“处理中”而不是“失败”,否则渠道其实已经受理,你这里返回失败,商户重试就是重复代付。

4.2 渠道适配层与状态转移

五合一代付系统的核心不是页面,是渠道适配层。一个干净的适配层由接口和路由组成:

// app/Services/Channel/ChannelAdapter.php interface ChannelAdapter { public function send(PayOrder $order): array; public function query(PayOrder $order): array; public function parseCallback(array $payload): array; } // app/Services/Channel/ChannelRouter.php class ChannelRouter { public function __construct(private array $adapters) {} public function get(PayOrder $order): ChannelAdapter { return $this->adapters[$order->channel_id] ?? throw new ChannelNotFoundException(); } }

send负责发起代付,query负责主动查单,parseCallback负责把渠道回调格式统一成标准化结果。ChannelRouter根据订单上的channel_id选适配器,而不是在 Controller 里写if ($channel == 1)。

状态转移是整个代付系统的命根子,建议按下面这张表实现:

原状态事件新状态约束条件
0 初始化渠道受理成功1 处理中渠道返回平台流水号
1 处理中回调成功2 成功验签通过,订单未超时
1 处理中回调失败3 失败失败原因明确
1 处理中超过 N 分钟未回调4 待复核不能自动置失败
0 初始化回调成功2 成功需要允许,见下方说明

注意最后一行的约束条件:回调可能在渠道受理的同时到达本系统,此时订单还没从 0 改成 1。如果回调逻辑只允许“处理中”一档更新,这条回调会被丢弃,订单永远卡死在初始化状态。

4.3 回调处理与主动查单兜底

回调接口最核心的一段是“原子更新”,先看代码:

public function callback(Request $request) { $raw = $request->getContent(); $payload = json_decode($raw, true); $adapter = $router->getByChannelId($payload['channel_id']); $result = $adapter->parseCallback($payload); if (!$result['ok']) { return ['code' => 0, 'msg' => 'skip']; } // 关键:只有 0 和 1 状态允许更新到终态,影响行数为 0 则该回调被忽略 $updated = PayOrder::where('order_no', $result['order_no']) ->whereIn('status', [0, 1]) ->update([ 'status' => $result['status'] === 'success' ? 2 : 3, 'platform_no' => $result['platform_no'], ]); if ($updated === 0) { return ['code' => 0, 'msg' => 'ignored']; } PayCallbackLog::create([ 'order_no' => $result['order_no'], 'raw' => $raw, 'status' => $result['status'], ]); return ['code' => 0, 'msg' => 'ok']; }

这里的whereIn('status', [0, 1])不是习惯问题,是必须。回调如果早于队列里的下单事务到达,订单状态可能还是 0,只允许 1 会漏掉回调。回调处理后顺手写PayCallbackLog,后续对账时如果发现订单状态和渠道账单不一致,这就是第一手证据。

主动查单用来兜住“渠道根本没回调”的情况。常见做法是 crontab 每分钟扫处理中订单,超过 10 分钟还没回调就去渠道查一次:

*/3 * * * * cd /data/www/pay_system && php bin/cli.php pay:confirm --minutes=10 >> storage/logs/confirm.log 2>&1

查单逻辑里不要直接把超时订单置为失败。渠道可能已经打出款了,只是没回调,此时置失败会让商户发起退款,然后渠道那边又真实打款,两边一交叉就是资金事故。超时订单应该进入“4 待复核”,等人工或对账文件来定夺。

5. 代付系统最容易翻车的几个坑:现象、原因、解决

5.1 重复代付:回调重试导致同一个单子打款两遍

现象:渠道因网络抖动重复推送同一笔成功回调,后台每次收到回调都执行了一次“更新状态 + 通知商户”的操作,结果商户侧可见重复放款。

原因:回调处理没有先做幂等,或者用“先查状态再更新”的非原子方式。两个并发回调同时查到状态是 1,同时更新,都成功,就重复了。

解决:用“原子更新”代替“查再改”。核心是UPDATE pay_order SET status=2 WHERE order_no=? AND status IN (0,1),影响行数为 0 的那次回调直接丢弃。Redis 锁可以做第一层拦截,数据库的状态约束才是最终防线。

5.2 回调地址配置错误,联调时回调迟迟不到

现象:本地启动项目后,商户下单成功,但渠道回调一直没有收到,日志里什么也没有。

原因:回调地址写成了127.0.0.1,或者用了内网地址但没有同步配置到渠道后台白名单。渠道回调是从渠道服务器访问你的服务,127.0.0.1指向它自己,永远到不了你的电脑;白名单里没有你的出口 IP,回调会被渠道直接拒绝。

解决:回调地址统一配置成线上可访问的 HTTPS 域名;本地联调时填写局域网 IP,并确保防火墙放行对应端口。测试环境至少用三层校验:回调 URI 固定、签名必须通过、请求来源 IP 可配白名单。

5.3 金额精度不对,对账差几分钱

现象:代付单金额是 0.1 元,数据库存储的是 0.099999,回调成功后再算手续费,平台数据比渠道账单少一分钱。

原因:金额字段用float或double存储。浮点数在二进制下无法精确表示十进制小数,累计到一定订单量后对账必然出差异。

解决:数据库统一用BIGINT存“分”,程序里接收金额时统一转分,四舍五入要显式写:

$amountFen = (int) round((float) $amount * 100);

这条代码看起来简单但很容易写错,(int) $amount * 100会把 0.1 先转成 0 再乘 100,结果变成 0,这是常见的玄学 BUG。

5.4 手续费没有预计算,月底资金对不上

现象:代付成功很多笔,月底服务平台应收手续费与上游扣费差异大,主账户余额对不上。

原因:手续费在渠道回调后才手工登记,没有在下单时按费率锁定。有的渠道按单笔限额收费,有的按最高上限,如果后台只算百分比,超限部分就漏掉了。

解决:下单或路由时根据channel_id的费率规则计算fee_fen并写入订单;如果渠道实际扣费和预计算差异大于阈值,订单进入“4 待复核”,不能自动成功。手续费表要和订单表分离,方便后续按渠道和结算周期汇总。

5.5 压缩包解压后页面路径被覆盖

现象:解压后源码可以运行,但几小时后页面样式错乱,或者多出一些莫名跳转。

原因:zip 内部存在同名文件或路径穿越,解压时某个config.php被后门版本覆盖。

解决:解压前先看文件列表,解压到独立目录,不要覆盖已有项目。对二手源码包,启动后重点检查三个可疑位置:公共目录下的index.php是否包含eval、base64_decode等关键字;控制器基类是否额外引入不明逻辑;composer.json里的依赖是否混入非公开包。不要迷信“号称原版”的源码,运行一个能进钱出钱的后门风险大于项目本身。

6. 从源码到能上线:先跑一次模拟网关演练

代付系统上线前,最值得做的一件事是搭一个模拟网关,把状态机、幂等和超时补偿都验证一遍。不要直接拿真实渠道去试,渠道联调环境有工作日限制,而且一笔测试款打给陌生人可能收不回来。

先把模拟网关开启,在配置里留出可控参数:

// config/mock.php return [ 'enabled' => env('MOCK_CHANNEL_ENABLED', true), 'delay' => 500, // 模拟渠道处理耗时,单位毫秒 'error_rate'=> 5, // 模拟失败比例,百分比 ];

然后按下面这组用例跑一遍:

用例输入预期结果
正常代付金额 100.10回调一次,订单状态 0 -> 1 -> 2
重复回调同一回调重放 10 次订单只成功一次,更新影响行数为 1
超时查单模拟网关延迟 30 秒订单进入 4 待复核,不自动失败
手续费拆分费率 2.5%预计算 fee_fen=2.5 元对应分,到账金额和费用分离
签名失败篡改 sign 后提交返回 40001,订单不创建

跑完这五条,才算这套代付系统在逻辑上立住了。注意超时用例要把 mock 的delay临时调到大于主动查询的阈值,触发pay:confirm的补偿路径,不然这一分支永远覆盖不到。

进阶用法是给美团/滴滴这类多模板加“渠道路由”,把模板和渠道绑定关系从代码挪到配置:

// config/channel_rule.php return [ 'default' => ['channel_id' => 1, 'template' => 'common'], 'rules' => [ ['min' => 1, 'max' => 2000, 'channel_id' => 2, 'template' => 'meituan'], ['min' => 2001, 'max' => 50000, 'channel_id' => 3, 'template' => 'didi'], ], ];

这个文件的意思很直接:金额在 0.01 到 20 元之间,美团商户走渠道 2,滴滴商户走渠道 3,超过 500 元统一走默认银行卡渠道。这样改模板不会影响后端,接新渠道时也不用手工改十几个 Controller。

最后说句私人的经验。我第一套代付系统上线前,跳过模拟网关,直接在正式渠道拿 1 块钱真实代付做验证,结果渠道受理成功但回调没到,前端显示失败,我发起退款,最后两笔钱来回折腾了四个小时才追回。从那以后我给自己定了条规矩:任何代付系统上线前,必须跑一遍模拟网关的五个用例,并且把测试记录截图放进发布单。这个习惯帮我挡掉过不止一次资金事故,也希望帮到你。

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

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

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

立即咨询