ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

美团代付多模版三合一源码拆解:策略模式实现三种代付场景

美团代付多模版三合一源码拆解:策略模式实现三种代付场景

简介:这份资源是美团代付业务的多模版三合一源码包,面向需要搭建代付系统的PHP开发者与二次开发人员,帮助快速落地代付功能并理解多模板切换的实现思路。包内共4个文件,以zip源码压缩包为主,另含sql数据库文件、txt说明文档与html页面文件,分别用于程序部署、数据导入与配置参考,整体约43.09MB。测试环境基于Nginx 1.2、MySQL 5.7与PHP 7.4,需安装ionCube、fileinfo、opcache、memcache、redis、swoole4、sg11、igbinary等扩展,并设置TP伪静态、运行目录指向public,导入数据库后修改.env文件完成数据库连接。资源附带教程,涵盖安装文件搜索与批量修改等环节,适合具备一定PHP与ThinkPHP基础的开发者参考。目前已有857人学习下载,可作为代付类项目搭建与多模板改造的实践素材。

1. 美团代付多模版三合一源码拆解:一套代码怎么跑通三种代付场景

外卖、团购、酒店这三个场景的代付逻辑,表面看都是「A 下单、B 掏钱」,实际落到代码里差别不小。外卖代付要处理配送地址和超时取消,团购代付要处理券码核销和退款回滚,酒店代付要处理预授权和到店核销。很多团队一开始只做外卖代付,等业务要扩到团购和酒店时,发现原来的代码根本改不动,只能推倒重来。美团代付多模版三合一源码要解决的就是这个问题:用一套统一的订单抽象层,把三种代付场景的差异收敛到配置和策略类里,公共的下单、支付回调、状态机、对账逻辑只写一遍。这篇笔记面向的是手里有这套源码、想把它跑起来并改造成自己业务的开发者,也适合正在设计代付系统的后端同学参考。我会按「先跑通最小闭环、再拆多模版设计、最后讲改造和避坑」的顺序展开,中间会给出可直接抄的配置和代码片段。

2. 把源码跑起来:环境、依赖与最小代付闭环

2.1 先看清目录结构再动手装依赖

拿到一套代付源码,别急着composer install或npm install。先花十分钟把目录结构看一遍,能省掉后面很多「文件找不到」的玄学问题。常见的三合一源码目录大致长这样:

project-root/ ├── app/ │ ├── controller/ # 入口控制器,按场景分 │ ├── service/ # 业务逻辑层 │ ├── model/ # 数据模型 │ └── pay/ # 支付相关,含代付策略 ├── config/ │ ├── pay.php # 支付渠道配置 │ └── scene.php # 三种场景的差异化配置 ├── public/ │ └── index.php # 入口文件 ├── runtime/ # 缓存和日志 └── sql/ └── install.sql # 建表语句

重点看app/pay/和config/scene.php。前者决定代付请求怎么发出去,后者决定三种场景的行为差异。如果源码里没有scene.php而是把场景判断硬编码在 service 里,说明这套代码的多模版设计做得不够干净,后面改造时要留意。

依赖安装按技术栈来。PHP 项目常见的是 ThinkPHP 或 Laravel 骨架,先确认composer.json里的 PHP 版本要求,别用 PHP 8.2 去跑只兼容 7.4 的老代码,会报一堆弃用警告。数据库一般用 MySQL 5.7 或 8.0,建表语句在sql/install.sql里,导入前先建好库并确认字符集是utf8mb4,否则用户昵称里的 emoji 会存进去变问号。

2.2 配置支付渠道与场景参数

代付能不能跑通,八成取决于配置对不对。以常见的微信代付和支付宝转账为例,config/pay.php里通常需要填这些:

<?php return [ 'wechat' => [ 'mch_id' => '你的商户号', 'app_id' => '你的应用ID', 'api_key' => 'API密钥', // 注意不是AppSecret 'cert_path' => '/path/to/apiclient_cert.pem', 'key_path' => '/path/to/apiclient_key.pem', ], 'alipay' => [ 'app_id' => '你的应用ID', 'private_key' => '应用私钥', 'public_key' => '支付宝公钥', ], // 代付场景开关,三合一的核心 'scene' => [ 'waimai' => ['enable' => true, 'timeout' => 900], // 外卖15分钟未支付取消 'tuangou' => ['enable' => true, 'timeout' => 1800], // 团购30分钟 'hotel' => ['enable' => true, 'timeout' => 3600], // 酒店1小时 ], ];

这里有几个参数容易填错。api_key在微信支付里是 APIv2 的密钥,不是应用密钥,填错了签名会一直失败但报错信息很模糊,只提示「签名错误」,排查起来很费时间。证书路径要用绝对路径,相对路径在不同入口文件下解析结果不一样,是典型的「本地能跑线上翻车」源头。timeout是代付订单的支付超时时间,单位秒,三种场景给不同值是合理的:外卖讲究快,团购可以等,酒店因为涉及预授权可以更宽松。

配置改完先别急着测支付,把runtime/目录权限给足,PHP 项目写不进日志会直接 500,而且错误信息被框架吞掉,只看到白屏。

2.3 用一条测试订单验证代付闭环

最小闭环的验证思路是:创建一个代付订单,拿到代付链接或二维码,模拟支付回调,看订单状态有没有正确流转。很多源码会带一个测试入口,比如public/test.php或命令行脚本。如果没有,可以自己写一个临时脚本:

<?php // test_daifu.php 放在 public 目录下,测完删掉 require __DIR__ . '/../vendor/autoload.php'; $app = new \app\service\DaifuService(); // 1. 创建代付订单 $order = $app->createOrder([ 'scene' => 'waimai', // 场景:外卖 'amount' => 0.01, // 金额,测试用1分 'payer_uid' => 1001, // 付款人 'owner_uid' => 1002, // 下单人 'out_trade_no' => 'TEST' . time(), ]); echo "订单号: " . $order['order_no'] . "\n"; echo "代付链接: " . $order['pay_url'] . "\n"; // 2. 模拟支付成功回调(实际由支付平台异步通知) $result = $app->handleNotify([ 'out_trade_no' => $order['order_no'], 'trade_status' => 'SUCCESS', ]); var_dump($result);

跑通这个脚本,看到订单状态从「待支付」变成「已支付」,说明代付主链路是通的。如果卡在创建订单就报错,先看数据库连接和表结构;如果卡在回调,重点查签名验证和订单号匹配逻辑。测试金额用 0.01 元,别用 1 元,代付接口一般有最低金额限制,但测试时小额更容易通过风控。

提示:测试回调时,如果源码用了file_get_contents('php://input')读取原始请求体,用浏览器直接访问是拿不到数据的,必须用 POST 且带正确的 Content-Type,建议用 curl 模拟。

3. 多模版三合一的设计:策略模式怎么落地

3.1 三种代付场景的差异到底在哪

要把三合一讲清楚,得先明确外卖、团购、酒店代付的差异点。我整理了一张对比表,这些差异直接决定了代码里哪些部分要抽象、哪些部分要隔离:

维度外卖代付团购代付酒店代付
订单超时15分钟,超时自动取消30分钟,可延长1小时,涉及预授权
退款规则未接单可全额退券未核销可退到店前可退,到店后扣费
状态流转待支付→已支付→配送中→完成待支付→已支付→待核销→已核销待支付→已支付→待入住→已入住
分账对象商家+骑手商家+平台酒店+平台
回调处理需通知商家接单需发券码需通知酒店预留

看这张表就明白,如果三种场景共用一套状态机,代码里会塞满if scene == 'waimai'这种判断,改一处影响三处。三合一源码的正确做法是:公共部分(下单、支付、签名、对账)抽到基类,差异部分(状态机、超时策略、退款规则)用策略类实现,通过配置决定加载哪个策略。

3.2 策略类的代码骨架与注册方式

一个可落地的策略模式骨架大概是这样。先定义接口:

<?php namespace app\pay\strategy; interface DaifuStrategyInterface { // 订单创建后的初始化 public function onCreated(array $order): void; // 支付成功后的处理 public function onPaid(array $order): void; // 超时未支付的处理 public function onTimeout(array $order): void; // 退款规则校验 public function canRefund(array $order): bool; }

然后三种场景各实现一个类,比如外卖的:

<?php namespace app\pay\strategy; class WaimaiStrategy implements DaifuStrategyInterface { public function onCreated(array $order): void { // 外卖下单后锁定配送时段 \app\model\Delivery::lockSlot($order['order_no']); } public function onPaid(array $order): void { // 通知商家接单 \app\service\NotifyService::toMerchant($order['merchant_id'], 'new_order'); } public function onTimeout(array $order): void { // 释放配送时段 \app\model\Delivery::releaseSlot($order['order_no']); } public function canRefund(array $order): bool { // 商家未接单才可退 return $order['status'] === 'paid' && !$order['accepted']; } }

注册方式用工厂或容器绑定,在config/scene.php里把场景名映射到类名:

'scene_map' => [ 'waimai' => \app\pay\strategy\WaimaiStrategy::class, 'tuangou' => \app\pay\strategy\TuangouStrategy::class, 'hotel' => \app\pay\strategy\HotelStrategy::class, ],

这样新增场景时,只要加一个策略类加一行配置,不用动主流程。这是三合一源码最值得学的地方,也是判断一套代付源码质量高低的关键:看它的场景差异是收敛在策略类里,还是散落在 service 的各个角落。

3.3 公共支付流程与场景钩子的衔接

策略类定义好了,主流程怎么调用它们?核心是在订单状态变更的关键节点插入钩子。以支付成功为例:

<?php namespace app\service; class DaifuService { public function handleNotify(array $notify): array { // 1. 验签,公共逻辑 if (!$this->verifySign($notify)) { return ['code' => 'FAIL', 'msg' => '签名错误']; } // 2. 查订单,公共逻辑 $order = \app\model\Order::where('order_no', $notify['out_trade_no'])->find(); if (!$order || $order['status'] !== 'pending') { return ['code' => 'FAIL', 'msg' => '订单状态异常']; } // 3. 更新状态,公共逻辑 $order->status = 'paid'; $order->paid_at = time(); $order->save(); // 4. 场景钩子,差异逻辑 $strategy = $this->getStrategy($order['scene']); $strategy->onPaid($order->toArray()); return ['code' => 'SUCCESS', 'msg' => 'OK']; } private function getStrategy(string $scene) { $map = config('scene.scene_map'); $class = $map[$scene] ?? null; if (!$class) { throw new \Exception("未知场景: {$scene}"); } return new $class(); } }

这段代码的价值在于:验签、查单、改状态这三步是三种场景完全一样的,写在主流程里;onPaid之后做什么,交给策略类。这样外卖策略去通知商家,团购策略去发券,酒店策略去通知预留,互不干扰。改造时如果发现某个场景需要额外的公共步骤,优先考虑能不能抽到主流程,而不是在策略类里重复写。

注意:策略类里不要直接操作数据库事务,事务边界应该由主流程控制。否则策略类里抛异常,主流程的事务回滚不掉,会出现订单状态和业务数据不一致。

4. 改造与二次开发:把源码变成自己的代付系统

4.1 替换支付渠道要动哪些文件

源码默认接的支付渠道不一定适合你的业务,替换渠道是二次开发里最常见也最容易出问题的操作。以把微信代付换成另一个渠道为例,需要动的地方有三处:config/pay.php里的渠道配置、app/pay/下的渠道实现类、以及回调入口的验签逻辑。渠道实现类通常要实现一个统一接口,比如createDaifuOrder、queryOrder、refund、verifyNotify四个方法。替换时保持接口不变,只换实现,主流程就不用动。

有个坑要提前说:不同渠道的代付到账时间不一样,有的实时到账,有的 T+1。如果你的业务逻辑假设「支付成功即到账」,换成 T+1 渠道后会出现用户已付款但收款方没收到的情况,客诉就来了。正确做法是在订单表里加一个settle_status字段,把「支付成功」和「结算完成」分开,策略类的onPaid里只处理支付成功,结算完成用另一个钩子。

4.2 新增第四种代付场景的步骤

三合一不是上限,业务发展后可能要加第四种场景,比如「跑腿代付」或「充值代付」。新增场景的步骤是固定的:第一步,在config/scene.php的scene_map里加一行映射;第二步,新建策略类实现DaifuStrategyInterface的四个方法;第三步,在数据库的场景字典表里加一条记录,让前端能选到这个场景;第四步,如果新场景有额外的状态,在订单表的状态字段里扩展枚举值,并检查所有用到状态判断的地方有没有遗漏。

第三步容易被忽略。很多源码的场景列表是硬编码在前端或配置里的,新增场景后后端通了但前端选不到,联调时才发现。建议把场景列表做成接口返回,前端动态渲染,这样加场景不用改前端。

4.3 对账与日志:代付系统不能省的后悔药

代付涉及真金白银,对账和日志是必须做的,但很多源码为了「简洁」把这块省了,后期出问题查都没法查。至少要保证三样东西:每笔代付请求的原始报文落库、每次回调的原始报文落库、每天和支付渠道对一次账。原始报文落库用单独的pay_log表,字段包括订单号、请求方向、报文内容、创建时间。对账脚本每天凌晨跑,拉取渠道的对账文件,和本地订单逐笔比对,差异记录到reconcile_diff表并告警。

日志这块有个血泪经验:不要把支付相关的日志和业务日志混在一起。支付日志单独一个文件,按天切割,保留至少 180 天。出问题时第一时间看支付日志,能省掉大量猜测。另外,日志里不要打印完整的密钥和证书内容,打印前几位做标识就行,避免日志泄露导致的安全问题。

5. 代付源码避坑排查:五个真实踩坑记录

5.1 回调重复触发导致重复发货

现象:用户支付一次,商家收到两条接单通知,团购场景下发了两次券。原因:支付渠道的回调可能重试,源码没有做幂等处理,每次回调都执行一遍onPaid。解决:在handleNotify里加幂等锁,用订单号做 key,处理前先SETNX,处理完再释放;或者更简单,更新订单状态时用where status = 'pending'做条件更新,影响行数为 0 就直接返回成功,不执行后续逻辑。

5.2 金额单位混用导致代付金额错 100 倍

现象:测试时代付 0.01 元正常,上线后用户代付 100 元实际扣了 1 元。原因:源码里有的地方用元,有的地方用分,创建订单时传的是元,调渠道接口时没乘 100。解决:全链路统一用分做单位,数据库存分,接口传分,只在展示层转成元。改造时全局搜索金额相关字段,确认单位一致,这个坑一旦上线就是资损。

5.3 超时取消任务把已支付订单也取消了

现象:用户支付成功后,过了超时时间订单被自动取消,钱退了但货也发了。原因:超时取消任务只判断了created_at + timeout < now,没判断订单当前状态。解决:取消前先查状态,只有pending状态才取消,用条件更新where status = 'pending'保证原子性。同时把超时任务的执行频率调高,比如每分钟一次,减少状态判断和实际取消之间的时间窗口。

5.4 证书路径在定时任务里失效

现象:网页端代付正常,定时任务里的退款全部失败,报证书找不到。原因:证书路径用了相对路径,网页端入口文件在public/下,定时任务入口在项目根目录,相对路径解析结果不同。解决:证书路径一律用绝对路径,在配置文件里用__DIR__或环境变量拼接,不要写./cert/xxx.pem这种。

5.5 场景配置漏改导致新场景走旧逻辑

现象:新增跑腿代付场景后,下单正常但支付成功后没通知跑腿员。原因:scene_map里加了映射,但策略类里onPaid方法忘了实现通知逻辑,或者复制了外卖策略但没改通知对象。解决:新增策略类时,四个接口方法都要有实现,哪怕暂时为空也要写注释说明;上线前用测试订单把每个场景的完整链路走一遍,重点看onPaid和onTimeout的副作用是否符合预期。

6. 进阶技巧:用配置驱动把三合一扩成 N 合一

前面讲的策略模式已经能支撑三合一,但如果场景继续增多,每加一个场景就写一个策略类,维护成本还是会上升。进阶做法是把策略类里可配置的部分抽出来,用配置驱动,只有真正特殊的逻辑才写代码。比如超时时间、退款规则、通知对象这些,都可以放到scene.php的配置里:

'scenes' => [ 'waimai' => [ 'timeout' => 900, 'refund_rule' => 'before_accept', // 接单前可退 'notify' => ['merchant', 'rider'], 'status_flow' => ['pending', 'paid', 'delivering', 'done'], ], 'paotui' => [ 'timeout' => 600, 'refund_rule' => 'before_pickup', 'notify' => ['merchant', 'runner'], 'status_flow' => ['pending', 'paid', 'picking', 'done'], ], ],

然后写一个通用的ConfigStrategy,根据配置里的refund_rule和notify执行对应逻辑。只有配置表达不了的场景,才单独写策略类。这样新增一个常规场景,改配置就行,不用写代码、不用发版。

验证这套机制是否可靠,我一般用两个方法。一是写一个场景配置校验脚本,检查每个场景的status_flow是否首尾正确、notify里的对象是否都有对应的通知实现、refund_rule是否是已知规则。二是做配置热更新测试,改完配置不重启服务,看新订单是否按新配置执行,老订单是否不受影响。这两个测试过了,配置驱动才算真正可用。

最后说个我自己的习惯:每次改完代付相关代码,不管改动多小,都用 0.01 元跑一遍三种场景的完整链路,从下单到回调到对账。这个习惯帮我拦下过好几次「看起来没问题」的改动。代付系统没有小事,一分钱的测试订单能省掉后面一万块的客诉。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进