
简介这份PHP外卖点餐源码是为餐饮行业及Web开发者准备的一套完整在线点餐系统解决方案涵盖用户注册登录、菜单浏览、下单支付、订单追踪等核心流程适合想快速搭建或二次开发外卖平台的初中级开发者参考。压缩包共1166个文件约5.13MB以193个php后端逻辑文件为主配以412个jpg、325个gif、38个png等图片素材以及83个js脚本、53个html页面、19个css样式和数据库db文件基本覆盖前端展示、交互、后端业务与数据存储。包内还附有源码说明、安装说明、系统流程图、使用前必读和更新日志等文档便于理解部署与排查问题。目前已有1814人学习下载对于需要完整参考实现、学习PHP项目结构或快速商用改版的开发者来说是一份性价比很高的实战代码库。1. PHP外卖点餐源码到底能做什么先看清这套源码的价值边界做餐饮小程序、H5 点餐PHP 外卖点餐源码一直是中小门店的首选方案。标题里的见面美观功能清晰翻译成开发者的实际需求就是前端界面让顾客觉得顺眼后台结构让维护的人不迷路。这套源码通常标配用户点餐端、商家管理端和数据库三块不依赖复杂中间件本地环境或一台入门云服务器就能跑落地成本很低。适合三类人想省平台抽成、自己做扫码点餐入口的餐饮小店老板想拿现成底子快速交付的接单开发者想读一条完整真实业务链路的 PHP 新手。边界也要看清——它服务的是门店日常流量不是大平台万人并发按轻量业务系统去评估就对了。2. 拆解外卖点餐源码的技术栈与数据库设计骨架是怎么立起来的2.1 PHP 选型与不同框架的取舍ThinkPHP 为什么在这类源码里出现最多我经手过的外卖点餐类源码绝大多数跑在 PHP 7.x MySQL 5.7 上。选 PHP 的理由很现实部署门槛低、普通虚拟主机就能跑、改完即生效完全匹配外卖点餐这种门店即开即用的业务节奏。但同样是 PHP框架选型直接决定你二次开发要花三天还是三周。常见三种形态原生 PHP 手写、ThinkPHP 系、Laravel 系。原生 PHP 没有目录约束文件平铺几万行代码叠起来后命名和参数传递非常容易乱Laravel 功能规范但依赖偏重对廉价虚拟主机不友好也很少在这种小业务源码里出现。中间位置就是 ThinkPHP自带 ORM 和模板引擎目录结构固定哪个文件管哪个模块一目了然中文文档覆盖广改源码的人不用靠猜。这不是说原生写不出好东西而是作为交付物ThinkPHP 的维护成本对后来接手的人更友好。对比项原生 PHPThinkPHPLaravel部署成本最低低中目录规范无强制MVC 分层固定MVC 分层严格二次开发友好度一般高中典型适用场景一次性活动页门店管理系统较重 Web 应用判断一份源码是不是 ThinkPHP 系很简单看有没有application或app目录入口文件是否指向public/index.php。再看是否用 composer 管理依赖ThinkPHP 5.x 以上基本都是 composer 装包源码一般自带vendor目录解压即用。我拿到源码的第一步不是急着跑而是先把整体目录过一遍确认入口、配置、前后台分离方式再动手改免得后面为迁就目录结构返工。2.2 订单、菜品、会员三张核心表字段设计里的几个硬规矩外卖点餐源码功能再花哨落到底都是十几张表在转。最核心的是菜品表、会员表、订单表其中订单表设计得好不好直接决定二次开发时想不想摔键盘。以下是我常用的订单主表结构也是这类业务最常见的做法CREATE TABLE order ( id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 订单号(业务唯一), user_id INT(11) NOT NULL COMMENT 下单会员ID, shop_id INT(11) NOT NULL COMMENT 所属门店ID, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 订单总金额, status TINYINT(4) NOT NULL DEFAULT 0 COMMENT 订单状态:0待支付,1待接单,2制作中,3配送中,4已完成,5已取消, remark VARCHAR(255) DEFAULT NULL COMMENT 用户备注, create_time INT(11) NOT NULL COMMENT 下单时间戳, pay_time INT(11) DEFAULT NULL COMMENT 支付时间戳, PRIMARY KEY (id), UNIQUE KEY idx_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;这里有几个不踩亏的硬规矩。第一业务唯一键选order_no而不是主键id客服查单、支付回调对账、退款核销全走订单号主键一旦暴露给外部接口还容易被人遍历数据。第二金额字段必须用DECIMAL(10,2)谁用 FLOAT 谁等着对账不平浮点精度问题在订单系统里是事故不是小事。第三时间字段用整型时间戳好处是 PHP 端想怎么格式化都行也不用担心 MySQL 和 PHP 时区设置不一致导致查询出来的时间差 8 小时。第四status用数字状态机而不是状态字符串程序判断和加索引都比字符串高效。订单明细表同样有讲究CREATE TABLE order_detail ( id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, order_id INT(11) NOT NULL COMMENT 订单主表ID, dish_id INT(11) NOT NULL COMMENT 菜品ID, dish_name VARCHAR(100) NOT NULL COMMENT 下单时菜品名称快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT(11) NOT NULL DEFAULT 1 COMMENT 数量, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;明细表里存dish_name和price快照是这套设计最容易被新手裁掉又最后悔的地方。菜品改名、调价是门店日常操作如果明细表只存dish_id关联菜品表历史订单的证据链就断了对账时价格对不上顾客投诉时商家也拿不出当时的凭据。快照字段的代价只是多一两列存储换来的是订单永远忠于下单那一刻的事实这笔账非常划算。注意金额运算不要用 PHP 浮点直接加减以分为单位计算或保持 DECIMAL能少掉一大半对账问题。会员表相对简单核心字段是手机号唯一、昵称、头像、余额、积分、注册时间。手机号用CHAR(11)加唯一索引单独建idx_mobile不要用VARCHAR加判断会让登录查询慢一个数量级。购物车表存user_id dish_id quantity即可地址表必须把收货人姓名、手机号、详细地址拆成三段存很多源码只存一个合并字符串打印小票和配送地图时还要拆非常被动。3. 本地把源码跑起来环境准备与最小启动命令3.1 PHP 运行环境怎么搭集成环境、面板还是手动编译拿到源码先别急着看代码把环境跑通是第一步。本地调试我一般用集成环境Windows 上常见 phpStudy 这类工具macOS 直接 brew 装 php 和 mysql服务器上用 Nginx PHP-FPM MySQL 标准组合。无论哪种PHP 版本先看源码声明7.x 最保险PHP 8 对部分老源码不友好each()、create_function()这类被移除的函数会让页面直接白屏。源码的安装说明里通常会写明版本要求装之前先看一眼。# Ubuntu/Debian 安装 PHP 7.4 及外卖源码必备扩展 sudo apt update sudo apt install -y php7.4 php7.4-fpm php7.4-mysql php7.4-curl php7.4-gd php7.4-mbstring # 确认版本和关键扩展 php -v php -m | grep -E pdo_mysql|mysqli|curl|gd|mbstring四个扩展一个都不能少pdo_mysql负责数据库访问curl是支付、短信这类外部接口的必须依赖gd管验证码和图片处理mbstring处理中文截断和编码转换。少了任意一个常见表现是安装页白屏、验证码不显示或者支付下单直接报错。装完扩展一定要重启 PHP-FPM否则新扩展不生效service php7.4-fpm restart这行很多人会漏。3.2 导入数据库与修改配置三处让新手反复折腾的参数这类源码的安装流程通常是建库 → 导入 SQL → 改配置 → 配伪静态。数据库导入用命令行最直观避免可视化工具导入大文件超时# 创建 utf8mb4 数据库并导入源码自带的 SQL 文件 mysql -uroot -p -e CREATE DATABASE takeout_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p takeout_db takeout.sql导入成功后打开数据库配置ThinkPHP 系的源码一般是application/database.php也有用.env的核心参数如下// 数据库配置文件改动后必须清缓存否则不生效 return [ type mysql, host 127.0.0.1, port 3306, database takeout_db, // 和上面创建的库名一致 username root, password 你的密码, // 本地环境建议单独建账号别用 root charset utf8mb4, prefix to_, // 表前缀和 SQL 文件里的前缀保持一致 ];配置里有三个坑。第一表前缀必须和 SQL 文件导出时的前缀一致很多源码表名是to_order、to_member你把前缀改成空或者改错页面会报表不存在。第二ThinkPHP 有配置缓存改完database.php如果不删除runtime目录下的缓存文件配置怎么改都不生效这是新手问得最多的问题。第三本地连数据库建议用127.0.0.1而不是localhost后者在某些环境下会走 Unix socket 而不是 TCP 端口连不上的时候先换成 IP 试一次。最后是网站根目录。ThinkPHP 的入口在public目录里Nginx 的root必须指到public否则访问首页会 500 或直接下载 index.php。伪静态配置是另一个高发问题server { listen 80; server_name takeout.local; root /var/www/takeout/public; # 重点入口目录不是项目根目录 index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.4-fpm.sock; } }配完伪静态重启 Nginx访问首页不出意外就能看到登录页或点餐页了。判断环境是否正常的快速办法先访问http://你的域名/index.php能出页面再访问http://你的域名/还能出页面说明伪静态生效前者行后者不行就是 rewrite 规则没生效。提示改完数据库配置后页面仍连旧库优先删除 runtime 目录缓存这是 ThinkPHP 系源码最常见的假死现象。4. 用户端与商家端功能拆解从加购到出餐的完整链路4.1 订单状态机一个外卖订单从创建到完成的状态流转外卖系统的核心不是页面好看而是订单状态不乱。我见过太多源码把状态写成一堆 if哪天商家多点两下按钮订单状态就错乱了。这类业务最可靠的做法是显式定义状态机和合法流转路径下面是常见状态定义状态码含义进入方式0待支付用户提交订单生成订单号和金额1待接单支付回调成功后自动置入2制作中商家在后台点击接单3配送中商家出餐骑手取餐4已完成用户确认收货或超时自动完成5已取消支付前取消、商家拒单或超时未支付写代码时用一个映射表约束合法流转比散落各处的 if 判断好维护得多/** * 修改订单状态带状态机约束防止非法流转 * * param int $orderId 订单ID * param int $newStatus 目标状态码 * throws \Exception 非法流转时抛出异常 */ public function changeStatus($orderId, $newStatus) { // 定义每个状态可以合法跳转的目标状态 $statusMap [ 0 [1, 5], // 待支付 - 待接单 / 已取消 1 [2, 5], // 待接单 - 制作中 / 已取消(拒单) 2 [3], // 制作中 - 配送中 3 [4], // 配送中 - 已完成 ]; $order Db::name(order)-where(id, $orderId)-find(); if (!$order) { throw new \Exception(订单不存在); } if (!isset($statusMap[$order[status]]) || !in_array($newStatus, $statusMap[$order[status]])) { throw new \Exception(非法状态流转: . $order[status] . - . $newStatus); } $data [status $newStatus]; if ($newStatus 1) { $data[pay_time] time(); // 进入待接单时记录支付时间 } Db::name(order)-where(id, $orderId)-update($data); }这段逻辑里有几个细节值得较真。支付回调更新状态时不要只传order_no就 update必须先查出当前状态再判断回调可能因为网络重试发多次每发一次就把状态覆盖一次订单就会从已完成被拉回待接单属于典型事故。另外进入待接单时记录pay_time比单独在支付表里记一笔更直接商家端排序时直接用这个字段排今天几点后的新单。changeStatus统一收口之后用户端取消、商家端接单、系统超时关单都调这一个方法状态就不会各写各的。4.2 商家端接单与出餐后台权限和本店操作的边界商家端是这个系统里被低估的一块。用户端做得再漂亮商家点半天没反应门店还是会换掉这套系统。商家后台的核心操作是查单、接单、出餐、拒单外加菜品上下架。这里最容易翻车的是权限边界很多源码的后台接口只校验是否登录不校验是不是本店的单子。public function accept() { $orderId (int) $this-request-post(order_id); $shopId $this-getLoginShopId(); // 当前登录商家对应的门店ID // 关键查询条件里带上 shop_id只能操作本店订单 $order Db::name(order) -where(id, $orderId) -where(shop_id, $shopId) -find(); if (!$order) { return json([code 1, msg 订单不存在]); } if ($order[status] ! 1) { return json([code 1, msg 订单状态已变更请刷新列表]); } Db::name(order)-where(id, $orderId)-update([ status 2, accept_time time(), ]); // 接单后按需触发通知公众号模板消息、短信或前端轮询 return json([code 0, msg 接单成功]); }where(shop_id, $shopId)这一行是关键所在。门店 A 的登录账号如果只按order_id操作把订单号换成门店 B 的订单号就能改别人家的单这在多门店部署场景里是实打实的安全漏洞。凡是做商家端接口查询条件里都要带当前登录人的门店 ID连菜品的修改、上下架也一样。接单后的通知链路常见做法是商家后台前端每 10 秒轮询一遍新订单数接口接单后再把状态推回用户端页面少数源码接入了公众号模板消息需要额外配置 appid 和 secret验签逻辑容易踩坑这个放到避坑章节细说。出餐环节通常还连着后厨小票常见做法是商家电脑上装一个小票打印服务后台点出餐时往本机端口写打印指令浏览器页面和打印服务之间走 HTTP 协议。这块各家实现差异很大集成前先确认打印机驱动兼容性不然店铺实机测试时会发现打印乱码或重复打印。如果源码不支持小票打印先跑通出餐后状态变为配送中这条主链路人工在厨师那边喊一嗓子也能撑住前期营业打印是锦上添花不是系统命脉。5. 外卖点餐源码部署与运行避坑记录5 个让开发者翻车的常见问题5.1 伪静态没配置内页全部 404现象首页正常点进菜品详情或商品列表全是 404路径带index.php?s/dish/12能打开但/dish/12打不开。原因没有配置伪静态 rewrite 规则。ThinkPHP 系的 URL 重写依赖 Nginx 或 Apache 把不存在的路径转交给index.php不配的话漂亮路由全部失效。解决Nginx 按 3.2 节里的location /rewrite 规则补上Apache 则在站点根目录放.htaccessIfModule mod_rewrite.c RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?s$1 [QSA,PT,L] /IfModule改完重启服务再刷新页面。判断标准就一条访问带index.php?s的地址能出页面去掉index.php也能出才算配好。5.2 明明账号密码都对数据库还是连不上现象安装页或登录页报数据库连接失败、Access denied for user但用命令行同样的账号密码能进 MySQL。原因通常有三种。第一配置里 host 写了localhostPHP 走 socket 而 MySQL 只监听了 TCP第二改完配置没有清 ThinkPHP 的 runtime 缓存第三数据库账号的 host 权限限制在localhost从127.0.0.1连被拒绝。解决先确认 MySQL 里账号的 host 授权是rootlocalhost还是root127.0.0.1统一配置和授权再试然后删除runtime目录下的全部缓存文件最后把 host 改成127.0.0.1强制走 TCP。三步做完九成以上的连接问题都能解决。5.3 图片上传成功但页面裂图菜品图不显示现象后台能上传菜品图片但前台菜品列表图片全裂或者上传时报目录不可写。原因上传目录权限不足或者gd扩展没装导致缩略图生成不了。很多源码上传原图后会生成缩略图gd缺失时原图存了、缩略图没生成页面引用的就是那张不存在的缩略图。解决检查 PHP 扩展列表里有没有gd没有就装上重启再确认上传目录权限常见路径是public/uploads# 给上传目录写权限Nginx/Apache 用户通常叫 www-data chown -R www-data:www-data /var/www/takeout/public/uploads chmod -R 755 /var/www/takeout/public/uploads改完重新上传一张图验证。如果缩略图路径是程序写死的二级目录还要检查该目录是否在源码包里存在有些源码默认不会自动创建需要手动建目录。5.4 本地调支付回调永远等不到现象本地环境用测试账号下单支付页面明明显示成功订单状态却一直是待支付因为支付平台的回调根本没进来。原因支付回调是支付平台服务器主动请求你的服务器本地开发机没有公网地址回调自然找不到门。解决本地调试不走真实回调用源码自带的模拟支付成功按钮跳过支付环节直接把订单置为待接单是最快的需要联调真实支付时用内网穿透工具把本地端口暴露到公网再把支付平台的回调地址改成穿透域名。注意改回调地址要同步改动源码里接收回调的控制器路由不然穿透进来了也验签失败。5.5 订单时间差了 8 小时对账对不上现象后台订单列表显示的下单时间比实际时间晚 8 小时或者反过来支付时间、接单时间全部偏移。原因PHP 的时区设置和 MySQL 的时区设置不一致典型是 PHP 用了PRC北京时间而 MySQL 的time_zone是SYSTEM跟随了服务器 UTC 时间。解决两处时区统一PHP 侧在入口文件或php.ini设置date_default_timezone_set(Asia/Shanghai);MySQL 侧在[mysqld]配置段加上default-time-zone 08:00或者当前会话执行SET time_zone 08:00改完重启 MySQL 和 PHP-FPM再新建一单验证时间是否对齐。如果源码里时间字段用的是整型时间戳问题会小很多因为时间戳不依赖时区格式化时才转换这也是我在第 2 章坚持用create_time存时间戳的原因。6. 上线前把源码打磨一遍性能优化与二次开发的三个关键动作源码能跑只是起点上线前我固定做三件事每一件都值得花半天时间。第一件事是给查询热点加缓存。外卖系统的查询热点很集中首页菜品、商家端新订单数。这两类数据变化不频繁却会被反复请求。常见做法是加一层文件缓存或 Redis// 商家端新订单数5 分钟缓存 $newOrderCount cache(shop_unread_ . $shopId); if (!$newOrderCount) { $newOrderCount Db::name(order) -where(shop_id, $shopId) -where(status, 1) -count(); cache(shop_unread_ . $shopId, $newOrderCount, 300); }注意接单操作里要顺手清除这个缓存否则商家端会一直显示旧的新单数。加缓存前先看源码用的什么缓存驱动ThinkPHP 默认文件缓存直接换 Redis 需要改配置和装扩展别在没确认的情况下硬切。第二件事是把菜品图片和 JS/CSS 静态资源交给对象存储或 CDN。外卖点餐页面图片多门店高峰期每个页面来回拉大图源站带宽很快吃满。常见做法是把public/uploads里的图片迁移到对象存储图片 URL 换成 CDN 域名迁移后记得验证缩略图生成逻辑还认不认新路径这一步最容易遗漏。第三件事是过一遍所有接收参数的地方。对外接口至少做三件事参数类型校验、order_no这类业务编号的格式校验、SQL 查询一律走参数绑定。我检查时会把搜索、订单查询、会员接口这几个入口单独拎出来看凡是拼字符串进 SQL 的地方全部改成占位符绑定。外卖订单涉及金额和隐私数据这里省下来的功夫上线后都会变成售后工单还回去。这套流程走完我还保留一个习惯每次改完配置先清缓存再重启服务然后点一遍下单 → 支付 → 接单 → 出餐 → 完成全链路。之前有次改门店信息接口自认为只影响商家端结果把用户端下单的shop_id查询条件带偏了全链路一测立刻暴露。线下的五秒检查永远比线上的半小时排查便宜。希望帮到你。本文还有配套的精品资源点击获取