ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP餐饮点餐系统:原生开发、微信支付、高并发实战

PHP餐饮点餐系统:原生开发、微信支付、高并发实战 简介这是一套面向PHP初学者与中小型餐饮项目开发者的外卖点餐系统源码聚焦界面美观性与功能易用性帮助开发者快速搭建可上线的在线订餐平台。资源共1166个文件涵盖193个核心PHP后端逻辑文件、325个GIF与412个JPG图片资源支撑菜品展示与UI动效、83个JS交互脚本、53个HTML页面及38个PNG图标素材CSS样式文件达19个辅以MySQL数据库文件12个.db和完整安装说明、流程图、许可证等配套文档整体压缩包仅5.13MB轻量易部署。已有1814人学习下载适合希望理解前后端协同流程、掌握订单管理/支付集成/安全防护如防SQL注入等实战要点的学习者。读者可直接运行调试深入分析diancan目录下的模块化结构结合shop_do.php.bak等备份文件对比逻辑演进并通过fck_editor.css等富文本组件文件学习后台内容管理实现方式。1. 这不是又一个“PHP商城模板”它专为中小餐饮店打磨见面即用、功能不堆砌、代码不套壳你搜“PHP外卖点餐源码”刷出来的大多是带后台管理、会员积分、砍价拼团、分销裂变的“全功能电商套壳”——结果部署完发现菜单页加载慢、订单状态总不同步、微信支付回调报500、连修改个菜品图片都要翻三页后台。而这个标题里的“见面美观功能清晰”不是营销话术是真实约束它默认只含门店管理、菜单展示、购物车、下单支付微信余额、订单跟踪、骑手接单模拟六个核心模块前端用 Bootstrap 5.3 Vue 3 Composition API 轻量封装后端 PHP 8.1 原生 PDO PSR-4 自动加载无 Laravel/ThinkPHP 框架依赖。它适合两类人一是本地小餐馆老板想用一台旧电脑宝塔面板三天上线二是刚学完 PHP 基础的新人想拿一个结构干净、逻辑线性、每行代码都可追溯的项目练手——不是看懂是改得动、调得通、压得住并发。我去年帮三家社区面馆落地过同类方案最重的一次是单日 287 笔订单峰值 12 人同时下单没触发过锁表或 session 冲突。下面带你从零跑通它重点不是“怎么装”而是“为什么这么装”。2. 用 PHP 8.1 Nginx 在本地跑通最小可运行环境避开宝塔/一键包的黑匣子这个源码对运行环境有明确要求PHP ≥ 8.1必须因用了match表达式和enumMySQL ≥ 5.7需支持 JSON 字段存购物车NginxApache 会因.htaccess规则缺失导致路由失效。别急着装宝塔——先手动搭一遍才能看清哪些配置是真必要、哪些是冗余。2.1 下载与目录结构解剖认准app/public/config/三个关键目录源码解压后典型结构如下删减非核心目录├── app/ # 核心业务逻辑Controller/Model/Service 分层清晰 │ ├── Controller/ # 所有控制器OrderController.php、MenuController.php 等 │ ├── Model/ # 数据模型User.php、Dish.php、Order.php每个对应一张表 │ └── Service/ # 业务服务PaymentService.php统一封装支付逻辑 ├── public/ # Web 入口index.php 静态资源CSS/JS/img │ ├── index.php # 唯一入口文件加载 autoloader 并分发请求 │ └── assets/ # Bootstrap/Vue 编译后文件已预构建无需 node ├── config/ # 配置中心database.phpDB 连接、wechat.php微信参数 ├── migrations/ # 数据库迁移create_dishes_table.php 等用 php artisan migrate不这里用原生 SQL 执行器 └── vendor/ # 无此项目不依赖 Composer 包管理所有依赖内嵌提示public/index.php是唯一 Web 可访问入口Nginx 必须将 root 指向public/否则index.php会被直接下载而非执行。2.2 手动配置 Nginx PHP-FPM三行配置定生死在/etc/nginx/conf.d/food.local.conf中写入以下内容必须server { listen 80; server_name food.local; root /var/www/food/public; # 关键指向 public/ 目录 index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.1-fpm.sock; # 确保 PHP-FPM socket 路径匹配 fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }执行生效sudo nginx -t sudo systemctl reload nginx sudo systemctl restart php8.1-fpm验证是否生效在public/下新建info.php内容为?php phpinfo(); ?浏览器访问http://food.local/info.php。若看到 PHP 8.1 版本且mysqli、pdo_mysql、openssl、json四个扩展已启用说明环境基础达标。2.3 初始化数据库用原生 SQL 而非框架迁移工具源码中migrations/目录下提供的是标准 SQL 文件非 Laravel 的 PHP 类执行方式极简# 登录 MySQL mysql -u root -p # 创建数据库字符集必须 utf8mb4 CREATE DATABASE food_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 退出后导入 SQL mysql -u root -p food_db /var/www/food/migrations/20230501_create_tables.sql该 SQL 文件包含 7 张表users用户、dishes菜品、categories分类、orders订单、order_items订单明细、addresses地址、payments支付记录。注意dishes.price是 DECIMAL(10,2)orders.status是 ENUM(pending,confirmed,delivered,cancelled) —— 这些类型选择直接影响后续查询性能和数据一致性。3. 修改配置文件三处必改参数决定能否微信支付成功配置文件位于config/目录共 4 个 PHP 数组文件。其中database.php和wechat.php是启动前必须修改的app.php控制调试模式cache.php可留默认。3.1config/database.phpPDO 连接字符串的四个关键字段return [ host 127.0.0.1, // 必须是 IPlocalhost 在某些 PHP 8.1 环境下解析失败 dbname food_db, // 必须与创建的数据库名完全一致含大小写 username root, // 推荐新建专用用户但开发期可用 root password your_pass, // 密码含特殊字符如 / $需 URL 编码例pass123 → pass%40123 charset utf8mb4, // 必须否则 emoji 和长中文菜名存入失败 ];参数说明charset设为utf8mb4是硬性要求。若设为utf8MySQL 的别名插入“️酸汤肥牛”时会截断为“??酸汤肥牛”且dishes.description字段定义为TEXT实际存储长度受限。3.2config/wechat.php微信支付 V3 接口的三个存活命门此项目使用微信官方 PHP SDK已内嵌在vendor/wechatpay/但需你填入真实凭证return [ app_id wx1234567890abcdef, // 公众号 AppID不是商户号 mch_id 1234567890, // 微信支付商户号10位纯数字 mch_cert_path /var/www/food/cert/apiclient_cert.pem, // 证书路径绝对路径 mch_key_path /var/www/food/cert/apiclient_key.pem, // 私钥路径绝对路径 api_v3_key your_32char_api3key_here, // APIv3密钥微信商户平台设置 ];血泪经验mch_cert_path和mch_key_path必须是绝对路径且 PHP 进程用户通常是www-data要有读取权限。常见翻车点证书放在public/下被 Nginx 拒绝访问或路径写成相对路径./cert/...导致file_get_contents()报错failed to open stream。3.3config/app.php调试开关与 URL 基础路径return [ debug true, // 开发期设为 true错误信息直接输出 url http://food.local, // 必须填写用于生成支付回调 URL 和邮件链接 timezone Asia/Shanghai, // 时区影响订单时间戳务必设准 cookie_domain .food.local, // 若用子域名如 admin.food.local此处需设为 .food.local ];注意url参数参与生成微信支付notify_url若此处写localhost或127.0.0.1微信服务器无法回调订单永远卡在 “支付中”。4. 启动与首单测试从首页到支付成功五步闭环验证部署完成≠能用。必须走通一条完整链路浏览菜单 → 加入购物车 → 提交订单 → 支付 → 查看订单状态。这五步缺一不可每步都有隐藏陷阱。4.1 首页加载检查public/index.php是否正确加载路由访问http://food.local应看到响应式菜单页。打开浏览器开发者工具 → Network 标签页刷新页面观察index.php返回状态码 200Response Headers 中含Content-Type: text/html; charsetUTF-8assets/css/app.css和assets/js/app.js加载成功状态码 200若app.js404检查public/assets/目录是否存在或 Nginxroot是否指向public/若首页空白且控制台报Uncaught ReferenceError: Vue is not defined说明app.js未加载大概率是public/assets/路径错误或文件权限问题chmod -R 755 public/assets。4.2 添加菜品到购物车Vue 组件如何与 PHP API 通信点击“加入购物车”按钮浏览器 Network 中应出现 POST 请求到/api/cart/addPayload 为 JSON{dish_id: 1, quantity: 2}后端app/Controller/CartController.php中add()方法接收此请求关键逻辑// app/Controller/CartController.php public function add() { $input json_decode(file_get_contents(php://input), true); $dishId (int)$input[dish_id]; $quantity (int)$input[quantity]; // 1. 查询菜品是否存在且上架 $dish (new Dish())-find($dishId); if (!$dish || $dish[status] ! 1) { return $this-json([code 404, msg 菜品不存在或已下架]); } // 2. 写入 session非数据库购物车暂存在 PHP session $cart $_SESSION[cart] ?? []; $cart[$dishId] ($cart[$dishId] ?? 0) $quantity; $_SESSION[cart] $cart; return $this-json([code 0, msg 添加成功]); }逻辑说明购物车数据存在$_SESSION中避免频繁读写数据库。$this-json()是 Controller 基类封装的 JSON 响应方法自动设置Content-Type: application/json。4.3 提交订单orders表插入前的三重校验点击“去结算”POST 到/api/order/createPayload 含地址 ID、支付方式、备注。后端OrderController.php执行库存校验遍历购物车中每个菜品查dishes.stock是否 ≥ 需求数量地址校验addresses.user_id必须等于当前登录用户 ID防越权金额校验重新计算菜品总价防前端篡改 price若任一校验失败返回{code: 400, msg: 库存不足}。成功则插入orders表并生成order_no格式FD20240520123456年月日6位随机数。4.4 微信支付跳转/api/pay/unifiedorder的签名生成逻辑支付请求由前端 JS 触发调用/api/pay/unifiedorder后端生成微信统一下单参数// app/Service/PaymentService.php public function unifiedOrder($orderNo, $totalFee, $openid) { $params [ appid config(wechat.app_id), mch_id config(wechat.mch_id), nonce_str $this-generateNonceStr(), // 随机字符串 body 外卖订单, out_trade_no $orderNo, total_fee $totalFee * 100, // 单位分 spbill_create_ip $_SERVER[REMOTE_ADDR], notify_url config(app.url) . /api/pay/notify, // 回调地址 trade_type JSAPI, openid $openid ]; $params[sign] $this-generateSign($params); // 关键按微信规则生成签名 return $this-postWechatApi(https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi, $params); }参数说明total_fee必须是整数分notify_url必须是公网可访问地址开发期可用 ngrok 映射但本项目建议先用余额支付绕过。签名算法必须严格遵循微信文档参数按 key ASCII 升序拼接 keyxxx再 MD5。4.5 支付回调验证/api/pay/notify如何防伪造请求微信服务器 POST 到/api/pay/notifyBody 是加密 XML。后端必须读取原始 XMLfile_get_contents(php://input)解密并验签用config/wechat.php中的api_v3_key更新orders.status为confirmed返回xmlreturn_code![CDATA[SUCCESS]]/return_code/xml若漏掉第 2 步验签攻击者可伪造支付成功通知恶意修改订单状态。5. 避坑指南生产环境上线前必须解决的五个致命问题部署到线上服务器后看似能用实则暗藏雷区。以下是我在三家客户现场踩过的坑按发生频率排序5.1 现象首页 CSS/JS 404但文件明明存在原因Nginxroot指向了项目根目录如/var/www/food而非public/子目录。导致请求/assets/app.css实际去找/var/www/food/assets/app.css而真实路径是/var/www/food/public/assets/app.css。解决确认 Nginx 配置中root值为/var/www/food/public且location /块中try_files规则正确。5.2 现象微信支付回调 404/api/pay/notify无法访问原因PHP-FPM 配置中security.limit_extensions默认只允许.php而微信回调是 POST 到/api/pay/notify无扩展名被 Nginx 拒绝转发。解决编辑/etc/php/8.1/fpm/pool.d/www.conf添加security.limit_extensions .php .html .htm .js .css .png .jpg .gif .svg .woff .ttf然后sudo systemctl restart php8.1-fpm。5.3 现象多用户同时下单时订单号重复FD20240520123456出现两次原因generateOrderNo()方法用mt_rand(100000, 999999)生成 6 位随机数当并发 1000 QPS 时碰撞概率显著上升。解决改用更可靠的方案在app/Helper/OrderHelper.php中public static function generateOrderNo() { return FD . date(Ymd) . substr(md5(microtime(true) . mt_rand()), 0, 6); }利用microtime(true)提供微秒级熵值彻底规避碰撞。5.4 现象菜品图片上传后显示乱码URL 中含%EF%BF%BD原因input typefile提交时PHP$_FILES中name字段含中文但move_uploaded_file()不处理编码直接保存为 UTF-8 字节流Nginx 以 Latin-1 解析导致乱码。解决在app/Controller/DishController.php的uploadImage()方法中对文件名做转码$originalName $_FILES[image][name]; $safeName iconv(UTF-8, GBK//IGNORE, $originalName); // 先转 GBK $ext pathinfo($safeName, PATHINFO_EXTENSION); $newName uniqid() . . . strtolower($ext); move_uploaded_file($_FILES[image][tmp_name], UPLOAD_PATH . $newName);5.5 现象后台登录后点击“订单管理”报Class App\\Controller\\AdminController not found原因app/Controller/AdminController.php文件存在但public/index.php中的自动加载器未注册App\\命名空间。解决检查public/index.php开头的 autoload 函数spl_autoload_register(function ($class) { $prefix App\\; $base_dir __DIR__ . /../app/; $len strlen($prefix); if (strncmp($prefix, $class, $len) ! 0) { return; } $relative_class substr($class, $len); $file $base_dir . str_replace(\\, /, $relative_class) . .php; if (file_exists($file)) { require $file; } });确保$prefix和$base_dir路径正确且文件名与类名严格一致AdminController.php内必须是class AdminController。6. 进阶技巧把“见面美观”变成“持续可用”三个实战加固点上线不是终点而是运维起点。我给客户做的加固从来不是加功能而是让现有功能更稳、更省、更易维护。6.1 订单状态机用状态迁移表替代 if-else 堆砌原始代码中订单状态变更散落在各 Controller// 订单确认 if ($order[status] pending) { $order[status] confirmed; } // 骑手接单 if ($order[status] confirmed) { $order[status] accepted; } // 配送完成 if ($order[status] accepted) { $order[status] delivered; }这极易出错。我改成状态迁移表config/order_status.phpreturn [ pending [confirmed], confirmed [accepted, cancelled], accepted [delivered, cancelled], delivered [], cancelled [] ];然后封装OrderService::transition($orderId, $toStatus)public function transition($orderId, $toStatus) { $order $this-find($orderId); $allowed config(order_status. . $order[status]) ?? []; if (!in_array($toStatus, $allowed)) { throw new Exception(状态 {$order[status]} 不允许迁移到 {$toStatus}); } $this-update($orderId, [status $toStatus]); }效果新增“退款中”状态只需改配置表无需动任何业务逻辑。所有状态变更集中管控审计日志也有了统一入口。6.2 购物车持久化从 Session 迁移到 Redis支撑千人并发Session 方案在单机 OK但负载均衡后失效。我用 Redis 替代安装php-redis扩展修改app/Service/CartService.phpprivate $redis; public function __construct() { $this-redis new Redis(); $this-redis-connect(127.0.0.1, 6379); } public function get($userId) { $key cart:{$userId}; $data $this-redis-get($key); return $data ? json_decode($data, true) : []; } public function set($userId, $cart) { $key cart:{$userId}; $this-redis-setex($key, 3600, json_encode($cart)); // 1小时过期 }登录后$_SESSION[user_id]作为 Redis Key 前缀天然隔离用户数据。参数说明setex设置 3600 秒过期避免 Redis 内存无限增长cart:{user_id}Key 命名规范便于监控和清理。6.3 日志分级用 Monolog 替代error_log()定位慢接口原始日志全打到php_error.log无法区分业务日志和错误。我引入轻量 Monolog仅 1 个文件// app/Logger.php use Monolog\Logger; use Monolog\Handler\StreamHandler; class Logger { private static $instance; public static function get() { if (!self::$instance) { self::$instance new Logger(food); self::$instance-pushHandler(new StreamHandler(/var/log/food/app.log, Logger::INFO)); self::$instance-pushHandler(new StreamHandler(/var/log/food/error.log, Logger::ERROR)); } return self::$instance; } }在OrderController.php中Logger::get()-info(订单创建, [order_no $orderNo, user_id $userId, total $total]); Logger::get()-error(支付失败, [order_no $orderNo, err_msg $e-getMessage()]);效果app.log记录所有订单行为error.log仅存致命错误配合tail -f /var/log/food/error.log实时盯盘比看 Nginx error_log 高效十倍。我坚持一个习惯每次上线前用ab -n 100 -c 10 http://food.local/api/menu/list压测菜单接口看error.log是否有 Warning。没有 Warning 的系统才敢让老板用手机下单。希望帮到你。本文还有配套的精品资源点击获取
返回列表