简介:这份源码包是一个基于PHP+MySQL的美团三合一系统,整合了外卖点餐、到店团购等常用业务模块,适合需要快速搭建本地生活服务平台的开发者或创业者使用。资源包共2011个文件,整体约83.07MB,主要包含817个JS交互脚本、308个PNG图片、271个HTML页面、172个CSS样式以及48个PHP后台逻辑文件,前台展示与后台管理兼顾,导入数据库并按说明配置环境后即可运行和二次开发。当前已有159人学习参考。包内附带了公众号授权、微信支付参数配置以及数据库连接等部署说明,可帮助使用者完成从环境搭建到支付联调的整体流程。整体而言,这套源码功能模块完整,适合熟悉ThinkPHP或有PHP基础的技术人员直接研究使用,也可作为本地生活类项目开发时的参考模板。
1. 美团三合一系统源码下载:先别急着跑,这套代码到底值不值得下
搜“美团三合一系统源码下载”的人,多半和我当年一样,手头有一个“做个本地生活平台”的需求,想找一份带用户端、商家端、管理后台的完整工程,直接跑起来看业务闭环。这个“三合一”在源码圈里通常指:一套订单体系同时撑起三种角色或三类业务,常见组合是用户点餐、商家接单、管理端看板,还有一类是外卖、团购、跑腿三种单据共用一个后端。它的价值在于让你不用从零写多端接口,适合全栈入门、接私活演示和课程设计参考。但这类包的水很深:二手转发的压缩包经常缺库、缺 SQL 脚本,前端 baseURL 写死成别人的域名,甚至有人在源码里藏定时任务。动工之前,先把下面这几件事捋清楚。
2. 三合一到底是哪三合一:源码里的三种组织方式与选型理由
2.1 三端合一:用户的 H5、商家的小程序、管理端的 PC
打开一份正常的三合一工程,最先看见的是三个前端目录加一个后端。常见做法是按端拆目录,而不是按页面拆,因为三端的发布节奏和权限边界完全不同。典型结构长这样:
meituan-like/ ├── admin/ # PC 管理后台,Vue3 + Element Plus ├── miniapp/ # 微信小程序端,uni-app 或原生小程序 ├── mobile/ # 用户 H5 端,Vue3 + Vant ├── server/ # 后端 API,PHP Laravel 或 Java Spring Boot └── sql/ # 数据库初始化脚本这个目录结构决定了你后续改动的工作量。admin管平台数据,miniapp和mobile管用户和商家,三端大部分接口是重合的,只在路由守卫上做区分。我一般会先读server/routes/api.php,看它是不是按/api/mobile、/api/merchant、/api/admin分段,这直接决定你新增接口是复用拦截器还是自己写权限判断。
参数上有一个全项目最容易被忽略的点:移动端的baseURL。很多源码默认写的是生产环境域名,你在本地跑起来之后,前端所有请求全打到别人的服务器上,页面就会白屏或者回一个诡异的数据结构。拿到源码第一件事,全局搜http://,把mobile/src/config/index.js和admin/src/utils/request.js里的 baseURL 改成http://127.0.0.1:8000/。顺带说一句,等跑通之后再读 Vue3 源码解析类的内容,你会更清楚多端工程为什么要用环境变量管理接口地址,而不是写死在配置里。
2.2 三角色合一:用户、商家、管理员如何共用一个登录态
有一部分三合一源码的“三合一”指角色,一套账号体系里同时存在用户、商家、管理员三种身份,但不会为商家单建一张用户表。主流设计是共用一个users表,加一个role字段区分,登录鉴权用 JWT + Redis。核心代码逻辑一般是这样的:
// server/app/Http/Middleware/JwtAuth.php public function handle($request, Closure $next) { $token = $request->bearerToken(); if (!$token) { return response()->json(['code' => 401, 'msg' => '未登录'], 401); } try { $payload = JWT::decode($token, env('JWT_SECRET'), ['HS256']); $request->attributes->set('user_id', $payload->uid); $request->attributes->set('role', $payload->role); } catch (\Exception $e) { return response()->json(['code' => 401, 'msg' => '登录已过期'], 401); } return $next($request); }这段中间件的逻辑很直白:从请求头里取 bearer token,解码后把uid和role塞进请求对象,后续控制器直接读这两个属性做数据过滤和权限判断。关键参数是JWT_SECRET,它一旦为空或者长度不够,所有和登录相关的接口都会报签名错误,后面第 5 章会专门讲这个坑。
和 Java 课程设计里常见的 Session + 拦截器组合不同,这类三合一源码几乎都偏无状态 Token,原因是 H5、小程序、PC 三端都要持久化登录态,Cookie 在 App 容器里最不稳定。商家端和小程序端共用同一套/api/merchant接口,前端拿到 token 后按角色跳转不同首页,后端只认 payload 里的role,这样你新增一个角色时不需要改中间件,只需要改路由分组。
2.3 三业务合一:外卖、团购、跑腿的单据如何落表
第三种“三合一”指业务层:一份源码里同时跑外卖订单、团购券订单、跑腿任务。核心就在订单主表的设计上,这类表的典型结构是:
CREATE TABLE `orders` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `order_sn` varchar(32) NOT NULL COMMENT '订单号', `order_type` tinyint NOT NULL DEFAULT 1 COMMENT '1外卖 2团购 3跑腿', `shop_id` bigint unsigned NOT NULL COMMENT '门店ID', `user_id` bigint unsigned NOT NULL COMMENT '下单用户ID', `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待付款 1已付款 2已接单 3配送中 4已完成 5已取消 6退款中', `pay_status` tinyint NOT NULL DEFAULT 0 COMMENT '0未支付 1已支付 2已退款', `total_amount` decimal(10,2) NOT NULL DEFAULT 0.00, `pay_time` datetime DEFAULT NULL, `created_at` timestamp NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`), KEY `idx_shop_status` (`shop_id`, `status`), KEY `idx_order_sn` (`order_sn`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='三合一订单主表';把三类业务塞进一张表,换来的是对账简单、后台列表一次查出全部单据;代价是订单明细必须按order_type去关联不同的子表,外卖关联order_goods,团购关联order_coupon,跑腿关联delivery_task。新手改这种表最容易犯的错是把status当成全局统一状态,实际上不同类型订单的状态流转并不同。比如团购券可以“未核销”,外卖没有这个概念;跑腿单没有“已接单”,而是“骑手已取件”。所以判断一份源码设计得好不好,就看状态字段是不是跟着order_type走的。
字段上的两个默认值要记住:status默认 0 表示待付款,pay_status默认 0 表示未支付。排查订单问题时,“待付款”是表里最干净的数据,优先从这里开始查状态机是否被改坏。
2.4 下载前先看这 4 个信号:判断教学版还是可交付版
先把边界说清楚:标着“美团三合一”的源码,九成是第三方开发者参照美团业务形态写的仿版或半成品,不是美团内部工程。它值得学,但直接拿去交付要谨慎。下载之前可以从四个信号判断它的成色。
第一看环境要求写没写具体版本。“PHP 7.4 + MySQL 5.7 + Redis 6”是好的信号,只写“前后端分离”的基本都是随口复制。第二看有没有数据库脚本和演示数据,缺 SQL 的包可跑程度大打折扣,说明发布者自己都没完整跑通过。第三看有没有加密痕迹,常见的是 ionCube、Zend Guard、入口文件里塞 eval 混淆,加密意味着你没法排查后门,也没法改功能。第四看最近维护时间,超过一年没动过的包,前端依赖大概率停在 Node 12 时代,装依赖都能折腾一晚上。教学版和可交付版之间最大的差别不是代码质量,而是有人真的拿它跑完过一条完整订单链路。
3. 从下载到跑通:部署“三合一”源码的最小命令集
3.1 下载源码后的第一件事:校验完整性和加密情况
解压之后别急着配环境,先做三个检查。这三条命令能在十分钟内告诉你这个包能不能信任:
# 解压后先数一数文件,正常的三合一工程前端加后端至少 500 个文件 find . -type f -not -path "*/node_modules/*" | wc -l # 检查是否存在可疑编码:eval + base64 是 PHP 源码里最常见的混淆特征 grep -rn "base64_decode" server/app server/config 2>/dev/null | head -n 20 # 检查是否带安装锁和数据库备份,很多二手包会把这些一起打包进来 find . -name "install.lock" -o -name "*.sql.bak" 2>/dev/null第一条命令帮你判断包是否完整。正常的工程文件数在 500 到 2000 之间,如果排除 node_modules 后只有几十个文件,大概率是阉割版。第二条命令最需要认真看,base64_decode本身是合法函数,正常代码里也会出现,但它通常只在工具类里用。如果你在控制器入口文件里看到eval(base64_decode("..."));这种一行代码完成整个文件工作的写法,基本可以确定这文件被人动过手脚。第三条命令是帮你识别这是不是“二道贩子”的复制包,install.lock存在说明系统已经在别的服务器上装过了,里面的配置信息不一定适合你。
如果发现有可疑文件,先看上下文,不要急于删。把匹配的文件名记下来,去对应的干净版本里比对,不少免费包里多出来的定时任务就藏在这种文件里。
3.2 环境准备:PHP 7.4/8.0 + MySQL 5.7 + Redis 的常见组合
这类源码八成是 PHP 写的,另外两成是 Java Spring Boot。PHP 系的运行成本最低,非常适合“源码建站”式的快速交付。我一般用 Docker Compose 起一套标准环境,而不是在本机一个个装中间件,省得被系统自带的 PHP 版本带偏:
services: mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: meituan3 ports: - "3306:3306" redis: image: redis:6-alpine ports: - "6379:6379" php: image: php:8.0-fpm volumes: - ./server:/var/www/html ports: - "9000:9000"这里有一个必须补的环节:PHP 镜像默认没装扩展。至少需要装pdo_mysql、redis、bcmath这三个,否则后面一定会报Call to undefined function Redis::connect()。装扩展在你的 Dockerfile 里加两行docker-php-ext-install pdo_mysql bcmath和pecl install redis就行。
提示:遇到
Call to undefined function Redis::connect()时,先别找代码问题,九成是 PHP 的 redis 扩展没装。
三合一工程的中间件依赖比普通单页应用多一个队列服务。订单推送、微信通知、短信验证码全都走队列,环境里少了 Redis,后端能启动但业务跑不动。这也是为什么我不建议直接用 Apache + 老 PHP 跑:环境差异会把“代码问题”和“环境问题”搅在一起,排错成本翻倍。
3.3 初始化项目:数据库迁移、密钥生成与本地域名配置
环境就绪后,进入源码的server目录开始初始化。以下命令按顺序执行,每一条都对应一个常见的失败点:
cd server cp .env.example .env # .env 里必须改的参数: # DB_HOST=127.0.0.1 # DB_DATABASE=meituan3 # DB_USERNAME=root # DB_PASSWORD=root # QUEUE_CONNECTION=redis # REDIS_HOST=127.0.0.1 php composer.phar install php artisan key:generate php artisan migrate --seed php artisan serve --host=0.0.0.0 --port=8000cp .env.example .env是为了让应用读取到环境变量。很多人直接改.env而没复制模板,结果APP_KEY为空,所有加密和签名功能全部报错。php artisan key:generate会把应用密钥写回.env,这是 Laravel 系 PHP 源码的必经步骤。migrate --seed建表并写入测试商家、测试商品、测试用户,种子数据决定了你登录时有没有东西可看。
QUEUE_CONNECTION=redis是这类源码的默认值,如果你不启动队列监听,订单状态会永远停在“已下单”,因为状态流转逻辑是通过事件触发队列去执行的。最后一行php artisan serve只是开发调试用,真上线要交给 Nginx 做伪静态和反向代理。
前端部分的启动命令相对固定:
cd mobile npm install npm run dev这里最常翻车的不是命令,而是 Node 版本。源码发布时间早的话,依赖里可能锁着 node-sass,Node 20 环境直接编译不过。常见做法是装 Node 14 + npm 6,或者看package.json里的engines字段,按它锁的版本走。
3.4 订单推送不执行:队列进程和 supervisor 守护
三合一源码跑起来之后,第一单会暴露最核心的问题:订单消息推不到商家端。原因不是 WebSocket 没连上,而是队列 worker 没启动。前端注册了事件,后端把任务丢进 Redis,但没人消费。开发环境手动启动一次就能验证:
php artisan queue:work redis --sleep=3 --tries=3--sleep=3表示空闲时等 3 秒再取下一个任务,--tries=3表示任务失败最多重试 3 次,超过就进入失败队列。上线环境不可能让进程一直在终端前台跑,要用 supervisor 守护:
# /etc/supervisor/conf.d/queue.conf 参考配置 # [program:meituan3-worker] # command=php /var/www/html/server/artisan queue:work redis --sleep=3 --tries=3 --timeout=90 # numprocs=2 # autorestart=truenumprocs=2是消费者进程数,小站点 2 个够用;--timeout=90防止某个慢任务卡死整个 worker。队列是排查订单问题时的第一道关卡:订单没反应先看 Redis 里queues:default的长度,确认任务是堆积了还是根本没进去。
4. 源码的关键逻辑:订单推送、支付回调与商家聚合的读写边界
4.1 订单状态机:从下单到完成的状态流转规则
三合一工程的业务核心不在页面,在订单状态机。先看一张状态定义表:
| status | 含义 | 可流转到 |
|---|---|---|
| 0 | 待付款 | 1、5 |
| 1 | 已付款 | 2、6 |
| 2 | 已接单 | 3、4 |
| 3 | 配送中 | 4、6 |
| 4 | 已完成 | 无 |
| 5 | 已取消 | 无 |
| 6 | 退款中 | 4 |
状态机在代码里的实现通常是一个校验数组 + 一个跳转方法:
// server/app/Services/OrderStateService.php private array $transitions = [ 'pending' => ['paid', 'cancel'], 'paid' => ['confirmed', 'refund'], 'confirmed' => ['delivering', 'completed'], 'delivering' => ['completed'], 'completed' => [], 'cancel' => [], ]; public function move(Order $order, string $target): bool { if (!in_array($target, $this->transitions[$order->status], true)) { throw new OrderStatusException("非法状态跳转: {$order->status} -> {$target}"); } $order->status = $target; return $order->save(); }这段代码的精髓在in_array(..., true),第三个参数是严格比较。PHP 里字符串'1'和整数1松散比较相等,如果不加true,用户传一个"delivering"可能被错误匹配到别的状态。很多翻车案例都是在这里栽的。
我见过不少教学版源码根本没有状态机,直接在控制器里$order->status = 2;一写了事。修改这类代码时要小心,如果哪一天加了“取消后重新支付”逻辑,这种散装赋值会让你满项目找状态更新点。判断一份源码业务功底怎么样,先看它有没有集中管理状态流转,这是三合一系统的地基。
4.2 支付回调的幂等:同一笔通知处理两次会怎样
支付回调是三合一系统里最容易出线上事故的地方。用户支付成功后,微信或支付宝会请求你服务器的回调地址;如果返回内容不对,支付渠道会按重试节奏反复推送同一笔订单。处理方法也很标准:
// server/app/Http/Controllers/Api/PayNotifyController.php public function notify(Request $request) { $orderSn = $request->input('order_sn'); $order = Order::where('order_sn', $orderSn)->first(); if (!$order) { return 'FAIL'; } // 幂等判断:已支付订单直接返回成功,不再处理 if ($order->pay_status === 1) { return 'SUCCESS'; } DB::transaction(function () use ($order) { $order->pay_status = 1; $order->status = 1; $order->save(); // 触发后续事件:发微信通知、创建配送单 event(new OrderPaid($order)); }); return 'SUCCESS'; }这段代码的关键不是事务,而是事务之前的pay_status === 1判断。请求进来先查一次订单,发现已经支付过就直接返回SUCCESS,避免重复入账和重复通知。没有这个判断的源码,用户付一笔,商家端能播报三遍新订单语音,因为支付渠道的重试请求最多能到十几次。
幂等判断之后才是事务和事件。event(new OrderPaid($order))是在事务提交前触发的,如果事件监听器里的消费者把微信通知发出去了,事务又失败回滚,会出现“通知发了但订单没改”的情况。稳妥做法是事务提交之后再用dispatch()->afterCommit()推事件,这样能保证一致。这是后来改源码时最值得补的一层防护。
4.3 商家聚合接口的读写边界:列表查询别把全库捞出来
三合一源码里被折腾得最多的是商家端订单列表。商家只应该看到自己店铺的单,但很多新手改着改着就把shop_id过滤丢了,结果商家 A 的账号能看到全平台所有订单。常见做法是这样的:
// server/app/Http/Controllers/Api/Merchant/OrderController.php public function index(Request $request) { $orders = Order::query() ->where('shop_id', $request->user()->shop_id) ->with(['goods', 'delivery']) ->when($request->status, fn ($q, $status) => $q->where('status', $status)) ->orderBy('id', 'desc') ->paginate($request->integer('pageSize', 10)); return response()->json($orders); }$request->user()->shop_id从中间件塞进去的用户对象里读当前店铺,这是数据隔离的边界。with(['goods', 'delivery'])用来预加载关联数据,没有它会出现经典的 N+1 查询:列表返回 10 条订单,代码会再执行 10 次子查询取商品和配送信息,接口耗时直接翻几倍。when()是条件查询,status参数不存在时自动忽略,避免前端传空值把 SQL 拼坏。
paginate()默认读page和per_page参数,这里用$request->integer('pageSize', 10)强制转成整型,防止有人传pageSize=abc让数据库报错。数据量变大后,这条 SQL 的瓶颈在orderBy('id', 'desc'),主键排序比created_at更快更稳,前提是订单表用自增主键。所以三合一系统的订单表都爱用bigint auto_increment,不只是为了简单,更是为了列表查询的性能。
5. 美团三合一源码常见问题与避坑:从白屏到后门的 5 个现场
5.1 现象一:前端白屏,接口飞到别人的域名
现象:H5 页面能打开,但页面空白,打开浏览器 Network 面板,请求的接口地址是https://xxx.com,根本不是你的本地服务。
原因:源码里的baseURL是写死的生产环境地址,没有走环境变量。发布者自己部署时用的是线上域名,打包时把这个地址留在了所有前端资源里。你跑npm run dev,请求照样往人家服务器发。
解决:全局搜前端目录里的http://和https://,把配置里的固定域名替换为/api,然后在 Nginx 里做代理:
location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; }替换完之后清一遍浏览器缓存,因为 Service Worker 可能已经把旧的接口地址缓下来了。这个坑几乎每一份二手三合一包都有,排查优先级排第一。
5.2 现象二:登录接口一直报“签名错误”
现象:用户名密码都对,但接口返回“签名错误”或“登录已过期”,刷新页面重登也不管用。
原因:.env里的JWT_SECRET是源码里写死的默认值,或者干脆就是JWT_SECRET=secret。代码里要求 32 位以上的随机字符串,长度不够或都用默认值,签出来的 token 在下次请求校验时对不上。
解决:在server目录下执行重新生成密钥并确认.env已更新:
php artisan jwt:secret生成后必须重启 PHP 服务才会生效,只改.env不重启等于没改。这个坑的隐蔽之处在于首次登录可能成功,因为签发和校验用的是同一个 secret,问题出在换了环境变量之后旧 token 全失效,表现为“登录状态不稳定”。
5.3 现象三:支付回调重复入账,商家播报三遍
现象:用户支付成功,商家端收到两条甚至三条新订单语音,数据库里订单金额没变,但通知重复执行。
原因:回调接口没有做幂等处理,后端的通知事件被重复消费。支付渠道重试回调,加上队列 worker 消费超时重试,一次支付触发多次业务逻辑。
解决:两层防护。第一层在数据库给orders.order_sn加唯一索引,从物理上防止同一笔订单被插入两次;第二层在回调代码开头判断pay_status === 1,已支付订单直接返回SUCCESS不再处理。之前 4.2 节那段代码就是这个问题的标准答卷。
5.4 现象四:图片上传 500,或换台机器图片全裂
现象:本地开发时图片能传,部署到服务器后上传接口 500;或者图片当时能打开,重启环境后裂图。
原因:storage目录不可写,或者public/storage符号链接没有建立。Laravel 系的 PHP 源码会把上传文件写到storage/app/public,再通过软链映射到public/storage,任何一步缺失都会导致图片读写失败。
解决:
chmod -R 775 storage bootstrap/cache php artisan storage:link有些源码不按 storage 规范走,直接把上传文件写到public/uploads,这种目录要额外确认权限。还有一点:下载包里如果带了public/storage,那通常是一个真实目录而不是软链,在 Linux 上部署时会表现怪异,删掉重建才是对的。
5.5 现象五:压缩包里多了个定时任务,部署后经常外连
现象:代码和数据库看起来都正常,但服务器 CPU 偶尔飙高,网络连接里频繁出现一个陌生的外网 IP,几分钟连一次。
原因:二手源码被二次处理过,发布者塞了定时任务,定期拉取外部地址或上报访问量。这类脚本常用一串base64_decode藏在入口文件里,也有的写在app/Console/Kernel.php的schedule里。
解决:先查系统定时任务,再看项目里的调度定义:
crontab -l # 检查源码里的外联请求 grep -rE "(file_get_contents|curl|wget).*(http)" server/app server/public --include="*.php"发现有写死的陌生域名回源,直接删掉对应文件。检查重点放在routes/web.php、public/index.php和app/Console/Kernel.php这三个位置,这是藏调度任务最常见的地方。这个操作不是多疑,免费二手源码里出现外联脚本的概率比你想的高得多。
6. 把“三合一”源码改造成能交付的项目:验证脚本与一个单号技巧
6.1 三合一订单号生成规则:一眼看出是哪笔业务
订单号用自增 id 有个问题:对账时看不出业务类型和店铺,而且能直接推算平台单量。我习惯把订单号改成“时间 + 类型 + 店铺 + 随机位”的 20 位结构:
$orderSn = date('YmdHis') . str_pad($orderType, 2, '0', STR_PAD_LEFT) . str_pad($shopId % 100, 2, '0', STR_PAD_LEFT) . str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT);生成结果类似202507211430010103456,拆开看:20250721143001是下单时间,01是外卖业务,03是店铺尾号,456是随机数。排障时扫一眼单号就知道是哪天的哪类单,不用开 SQL 查。注意random_int是加密安全的随机函数,比mt_rand更抗猜测。订单号拼接完后要查一次重,数据库order_sn加唯一索引兜底。
6.2 半小时跑完的回归链路:用脚本替手点
改完状态机或支付逻辑后,最怕的就是手点页面点到手软还漏了一条链路。我常用 curl 直接打核心接口做回归:
# 模拟用户下单 curl -X POST http://127.0.0.1:8000/api/mobile/order \ -H "Authorization: Bearer $USER_TOKEN" \ -d "goods_id=1&num=1&shop_id=1" # 模拟支付平台回调 curl -X POST http://127.0.0.1:8000/api/pay/notify \ -d "order_sn=202507211430010103456&pay_trade_no=TEST001" # 模拟商家接单 curl -X POST http://127.0.0.1:8000/api/merchant/order/confirm \ -H "Authorization: Bearer $MERCHANT_TOKEN" \ -d "order_sn=202507211430010103456"跑完后再查一次订单状态:下单后pay_status=0 status=0,回调后pay_status=1 status=1,接单后status=2。任何一步对不上,说明状态机在改的过程中被破坏了。这套脚本我每改一次业务代码就跑一遍,比什么都管用。
我当年拿一套三合一源码直接上线,跳过了支付回调的幂等验证,结果用户付一笔门店播报三遍,最后只能手动对账,费了整整两天。后来不管接什么源码,第一件事就是把订单链路脚本连跑十遍,确认状态机没有重复消费和非法跳转。这个习惯帮我挡掉了至少五次线上事故。希望帮到你。
本文还有配套的精品资源,点击获取