ARTICLE DETAIL

资讯详情

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

双框架实战:ThinkPHP+Laravel构建微信小程序餐饮点单系统

双框架实战:ThinkPHP+Laravel构建微信小程序餐饮点单系统 1. 一个点单系统为什么同时出现ThinkPHP和Laravel两套框架先把这个项目最大的争议点说清楚标题里同时出现ThinkPHP和Laravel很多人第一反应是写错了或者觉得是拿两个框架做对比演示。实际上在我做这个餐饮店内点单系统的过程中双框架是刻意为之的分工方案而且这个组合在中小型餐饮SaaS项目里并不少见。我接手这个项目时客户方已经有了一个跑了一段时间的ThinkPHP 6管理后台里面存着菜品分类、菜品列表、桌台管理、员工账号这些基础数据。但小程序端要用的订单API、支付回调、消息推送这些实时性较强的接口他们希望我用Laravel来写。原因很实际团队里几个新人只熟悉Laravel的路由和中间件机制ThinkPHP那套代码又不敢大动怕影响线上数据。于是整个系统的架构就变成了下面这样ThinkPHP 6负责管理后台包括菜品维护、桌台维护、员工账号与角色管理、营业数据报表。这套系统面向餐厅掌柜和店长低频操作对并发要求不高主要追求稳。Laravel 8负责小程序端所有API包括登录鉴权、开台下单、加菜退菜、后厨取餐通知、订单状态流转。这套系统面向服务员、后厨、收银员高峰期并发集中需要良好的中间件和队列支持。微信小程序一个项目通过不同角色入口加载不同页面服务员端和后厨端共用一套基础组件但页面权限完全隔离。有人会问为什么不用一个框架全搞定因为现实项目不是从零开始的绿地项目而是有历史包袱。ThinkPHP后台已经在稳定运行上面挂着菜品图片的磁盘路径、历史订单数据、甚至一些Excel导出功能迁移成本远大于维护两套框架的成本。与其推翻重来不如在Laravel里通过HTTP请求去读取ThinkPHP的数据库两个服务共享同一个MySQL实例只是各自用各自的ORM模型。这个决策在项目验收时被客户方认可因为两套系统可以独立升级、独立部署互不拖累。如果你也在做一个类似的餐饮系统我的建议是不用迷信“一个项目必须用一套框架”而是先盘点存量代码和团队能力。两个框架共用一个数据库完全可以用合理的表前缀或独立库来隔离两个服务之间如果需要同步数据直接读写同一张表就行不需要搞复杂的消息队列。关键是把职责边界划清楚别让两套框架的代码互相调用。1.1 两套框架各自的数据库访问策略双框架最容易出问题的地方就是两边同时对同一张表做结构变更。我在这项目里定了几条规矩强烈建议你照抄表结构变更必须走迁移脚本Laravel侧用MigrationThinkPHP侧用单独的SQL文件管理两边都要留存记录。公共字典表如订单状态、支付方式由ThinkPHP后台维护Laravel API只读不写。业务流水表如订单表、订单明细表由Laravel主导写入ThinkPHP后台只做查询展示。我在Laravel里通过config/database.php配置了多个MySQL连接一个laravel_order连接指向订单库一个thinkphp_platform连接指向基础资料库。模型上区分清楚// Laravel 中的菜品模型指向ThinkPHP维护的基础资料库 class Dish extends Model { protected $connection thinkphp_platform; protected $table tp_dish; protected $primaryKey dish_id; public $timestamps false; }ThinkPHP那边同理用protected $connection或直接在数据库配置里增加mysql_order连接来访问Laravel写入的订单表。这样两个服务都只操作各自负责的表偶尔需要跨库联查时也尽量通过冗余字段或单独接口解决坚决不写跨库JOIN。实测下来数据一致性没有出现过问题因为餐厅类系统的数据量级远没到需要分库分表的程度双框架共库是性价比最高的方案。2. 餐饮店内点单的业务链路拆解从服务员开台到后厨出餐这个系统的核心价值不是做了一个“小程序的点单页面”而是把店内服务流程的每一个环节都搬到了线上。我先把业务链路完整列一遍你会发现每一步都是独立的模块模块之间靠订单状态串起来。店内点单完整流程是这样的顾客进店入座桌上的二维码标识桌台号顾客扫码后进入小程序点餐页。顾客选菜、加菜、确认下单订单状态变为“待支付”或“待确认”线下场景通常由服务员确认。服务员通过服务员端看到新订单可确认订单也可以帮顾客直接加菜、退菜、转台、并台。订单确认后后厨端实时出现新订单后厨按单制作。后厨完成一单或一道菜后点击“出餐”或“叫号”服务员端收到通知完成上菜。顾客用餐结束服务员发起结账收银台完成支付订单状态变为“已完成”。这套链路里最核心的不是点单页面而是订单状态机。我一开始设计时走了弯路把“支付状态”和“制作状态”混在一个字段里导致后厨看到已支付的订单不知道要不要做服务员看到已制作的订单不知道有没有上菜。后来重构为两个独立的字段项目才算跑顺。2.1 订单状态与制作状态的分离设计在订单表里我拆成了order_status和cooking_status两个字段分别记录财务流转和制作流转。order_status的取值值含义触发场景1待确认顾客提交订单等待服务员确认2已确认服务员确认接单进入制作队列3已完成已结账离店订单归档4已取消超时未确认或顾客主动取消5已退款结账后发生退款cooking_status的取值值含义触发场景0未开始订单刚确认后厨还没开始做1制作中后厨点击“开始制作”锁定某个后厨成员2已完成后厨制作完成等待上菜3已上菜服务员确认上菜4已退菜顾客退菜或菜品估清取消两个字段互不干扰一笔订单可以先完成制作但订单的支付状态还没确认也可以客人先结账但后厨还在制作加点的菜。这种设计在餐饮门店特别重要因为高峰期经常出现“菜还没上完客人就要结账”的情况如果用一个状态字段后厨根本没法判断该不该继续做。2.2 桌台状态的流转逻辑桌台是店内点单区别于外卖点单的核心概念。我将桌台状态设计为“空闲、占用、待清洁”三态顾客扫码选桌后桌台变为“占用”同时生成一个进行中的订单。顾客结账完成后桌台变为“待清洁”。服务员在服务员端点击“清洁完成”桌台恢复“空闲”。桌台状态更新和订单状态绑定在一起通过Laravel的模型事件自动完成。比如在订单模型写入completed_at时自动触发桌台状态更新。这样避免服务员忘记手动改桌台状态系统自己就能闭环。最开始我用的是服务员手动改桌台状态高峰期经常忘记导致下一桌客人扫码时提示“桌台已占用”体验很差。改成自动联动之后这个问题基本消失了。3. 微信小程序端的分角色实现服务员端与后厨端是怎样共存的小程序端的难点不在页面数量而在于同一个代码仓库里要跑出两套完全不同的工作台。顾客端扫码点餐、服务员端管理订单、后厨端查看制作队列三者权限、页面、交互都不一样。我在项目里用了“角色入口 分包加载”的方式来实现。3.1 小程序的目录结构与分包策略我把小程序分成四个主包之外的子包pages_customer顾客扫码点餐页面包括菜品分类、菜品列表、购物车、确认订单、支付结果页。pages_waiter服务员工作台包括当前桌台列表、订单详情、加菜/退菜操作、并台转台、结账操作。pages_kitchen后厨工作台包括制作队列、订单详情、制作完成/开始制作操作、催单提醒。pages_common登录页、个人中心、订单历史、消息中心等公共页面。每个分包在app.json里配置独立路由通过subpackages字段声明。顾客扫码进入时默认加载pages_customer分包服务员和店员通过验证员工账号后进入对应分包。这样做的好处是小程序启动时不用加载全部页面代码首屏速度明显提升对门店里那些配置不高的安卓收银机特别友好。3.2 服务员端的核心操作设计服务员端是整个系统里交互最重的部分。我用小程序自带的button组件配合自定义弹窗实现了几个高频操作开台扫码进入后选择桌台人数一键开台自动生成订单号订单号规则是T 日期 四位流水号例如T202501150023。加菜在订单详情页点击加菜复用顾客端的菜品选择组件已选的菜品出现在同一个购物车里提交后生成新的订单明细不改动原有明细的记录结构。退菜选择要退的菜品填写退菜原因可选客人要求、菜品估清、做错等提交后进入后厨端确认流程避免服务员随意退菜导致后厨出品混乱。并台和转台将A桌订单合并到B桌或把订单从一个桌台转移到另一个桌台同时更新桌台状态。这些操作的后端接口都在Laravel里实现服务员端调用时通过角色鉴权中间件判断当前登录用户是否有权限。管理后台ThinkPHP给服务员账号分配角色权限点比如“允许退菜”“允许并台”没有权限点的操作在小程序端直接隐藏这是当初为了避免新人误操作才加上去的权限控制。3.3 后厨端的队列视图与状态操作后厨端相对简单但有一个细节需要特别设计大屏模式。后厨通常会把iPad或电视挂墙上来显示订单所以我在后厨端做了一个自动刷新的全屏队列视图订单卡片按“下单时间先后排序”新订单置顶并伴有语音提示。每张订单卡片显示桌台号大号字体下单时间精确到分钟菜品列表每项显示名称、规格大份/小份和数量制作状态标签未开始/制作中/已完成催单次数如果顾客通过小程序催单这里会亮红后厨的操作只有两个按钮“开始制作”和“制作完成”。为什么不做“上菜”按钮因为上菜动作属于服务员后厨只需要把菜放到出餐口。这样职责分离避免后厨点错按钮导致菜没人上。3.4 微信小程序登录态与员工鉴权小程序端登录我采用典型的wx.login换取openid再用openid绑定员工账号的机制。具体流程小程序端调用wx.login()获取临时code传给Laravel的/api/auth/login接口。Laravel拿着code调用微信接口换取openid和session_key。服务端查employees表看这个openid是否绑定了员工账号。如果没绑定返回绑定引导页已经绑定则签发token。后续请求在Authorization头携带tokenLaravel通过自定义中间件解析并注入当前登录员工信息。Token我用了laravel/passport的简化版实现其实就是tymon/jwt-auth因为当时不想为这个项目引入完整的OAuth体系。JWT的有效期设置成12小时覆盖一个班次。到期后小程序端收到401自动跳转登录页重新静默登录用户无感知。4. 双框架下核心接口设计与数据表结构说实话这个项目里最有复用价值的部分不是代码而是接口契约和表结构设计。微信小程序、Laravel API、ThinkPHP后台三方联调最怕的就是字段名不一致。我在项目初期就定义好一份接口文档所有联调都以文档为准遇到任何歧义先改文档再改代码。4.1 核心数据表结构订单相关表我设计成五张orders订单主表字段包括id、order_no、table_id、customer_count、order_status、cooking_status、total_amount、discount_amount、pay_status、remark、created_at、finished_at等。order_items订单明细表字段包括id、order_id、dish_id、dish_name、spec、quantity、price、item_status用于单菜退菜状态。tables桌台表字段包括id、table_no、floor_area、seats、table_status。employees员工表字段包括id、openid、name、role、work_no、phone。dishes菜品表沿用ThinkPHP后台的tp_dish表Laravel通过独立连接读取。订单表和订单明细表采用主从表结构一个订单对应多条明细。所有涉及金额的字段都用DECIMAL(10,2)不直接使用FLOAT避免微信支付对账时出现分差。金额计算统一在后端完成小程序端只是展示前端永远不能提交总金额只能提交菜品ID和数量这能避免用户篡改价格。4.2 Laravel侧的关键接口设计我列出几个核心API的设计思路便于你理解整个接口层的粒度。POST /api/order/create创建订单。请求体大致是{ table_id: 12, customer_count: 4, items: [ {dish_id: 101, spec: 大份, quantity: 2}, {dish_id: 102, spec: , quantity: 1} ], remark: 不要辣 }后端处理逻辑校验桌台状态不能是待清洁或已占用防止重复开台。校验菜品是否存在、是否上架、份量规格是否匹配。计算总金额生成订单主记录状态设为“待确认”。批量插入订单明细。更新桌台状态为“占用”。通过事件推送新订单消息到后厨端。写入操作全部放在事务里任何一个步骤失败就整体回滚。这里有一个实际踩过的坑如果先插了主表再插明细明细插入出错时事务回滚但主表自增ID已经消耗订单号会出现跳号。后来我把订单号改成独立生成在事务外先取号再在事务里写入问题就解决了。POST /api/order/confirm服务员确认订单。确认之后才进入后厨制作队列。这个接口会触发一个事件OrderConfirmed监听器把订单写入Redis的有序集合作为后厨队列使用同时通过WebSocket向前端推送新订单提示。GET /api/kitchen/queue后厨队列列表。从Redis有序集合中按时间排序取出未完成订单返回给后厨端。由于队列数据在Redis里接口响应速度非常快实测高峰期也能稳定在20ms以内。4.3 ThinkPHP后台的菜品管理与报表ThinkPHP这边主要做管理功能接口不多但有一个接口需要特别注意菜品上下架接口。下架一个菜品时不仅要更新tp_dish表的状态还要同步清理小程序端购物车里的已选菜品缓存。因为顾客在小程序端把菜加入购物车后如果后台突然下架了这个菜下单时就会报错。我在ThinkPHP后台的下架接口里额外调用了Redis的清理方法把包含该菜品的购物车缓存标记为失效顾客端下次打开时自动移除并提示“部分菜品已售罄”。5. 订单实时联动从轮询到WebSocket走过的弯后厨端要实时看到新订单服务员端要实时收到催单和上菜通知这是店内点单系统的刚需。但小程序的特殊性在于它不是常驻进程切到后台时WebSocket会被微信挂起。所以我做了两种方案结合。5.1 轮询方案简单但够用我先用轮询做了一套最低可用版本后厨端每10秒请求一次/api/kitchen/queue服务员端每10秒请求一次/api/waiter/notifications。当时觉得单店并发不高10秒轮询完全够用。实测下来10秒轮询在门店正常营业时确实没毛病。但高峰期有顾客催单时后厨最怕的就是“催单消息延迟10秒才看到”顾客已经和服务员吵起来了后厨还没反应。所以我后来加了WebSocket推送。5.2 WebSocket推送方案基于GatewayWorker的实践在Laravel框架里做WebSocket很多人第一时间想到laravel-websockets但这个包在服务器资源紧张的小内存VPS上跑起来很吃力。我换了一种方案用独立的GatewayWorker进程处理WebSocket长连接Laravel通过HTTP调用GatewayWorker的API做消息推送。服务器上的部署结构是Nginx监听80/443端口路由到两个后端服务Laravel APIphp-fpm和GatewayWorkerwebsocket端口8282。小程序端通过wx.connectSocket({ url: wss://你的域名:8282 })建立长连接。Laravel收到订单确认事件后调用GatewayWorker的内部推送API把消息发给对应角色的客户端连接。GatewayWorker的客户端连接是根据role分组的后厨端连接时带上typekitchen服务员端带typewaiter推送时按组广播。比如新订单只推送给后厨组不推送给服务员组因为服务员不需要知道“谁做了这个菜”。5.3 小程序端长连接的处理细节小程序端我封装了一个socket.js核心流程页面onLoad时如果当前用户是后厨角色就建立WebSocket连接。连接成功后发送一个绑定消息包含employee_id和role。收到服务端心跳ping时回发pong避免连接被微信或Nginx切断。收到new_order消息时后厨端页面刷新队列并调用wx.playVoice播放提示音。连接异常断开时onSocketError/onSocketClose通过setTimeout做5秒后自动重连。这里的重连机制是必须的。我曾经只监听onSocketError忽略了onSocketClose结果小程序切后台再切回来之后连接已经静默断掉了后厨端再也收不到新订单提醒。后来补上两个事件的统一重连处理才算稳定。6. 部署上线与实测中的几个关键坑最后这部分我把这个项目从部署到上线过程中踩过的坑集中列一遍每一件都是真金白银换来的经验。如果你照着做至少能省下三五天的排查时间。6.1 小程序合法域名与HTTPS证书配置微信小程序有个硬性要求所有请求域名必须是HTTPS且在小程序后台配置合法域名。我在一开始就把Laravel API和GatewayWorker都部署在同一个域名下用Nginx做路径分发这样只需要配置一次合法域名。Nginx配置示例server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/example.pem; ssl_certificate_key /etc/nginx/ssl/example.key; # Laravel API location /api/ { proxy_pass http://127.0.0.1:9000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # WebSocket location /ws/ { proxy_pass http://127.0.0.1:8282; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection Upgrade; } }特别注意proxy_set_header Connection Upgrade这个配置少了它WebSocket连接会被Nginx一直接收但客户端一直收不到消息而且不报错极难排查。我当时花了半天时间才发现是Nginx没把Upgrade头转发过去。6.2 小程序码与桌台二维码的生成店里每个桌子上贴的二维码指向的是小程序的pages_customer/scan页面后面带一个参数table_id。我最初用微信提供的wxacode.getUnlimited接口来生成小程序码但发现这个接口有数量限制一次最多生成10万张。后来改成所有桌台共用一个参数模板table_id通过URL参数拼接小程序端解析出来再传给后台。二维码生成的代码逻辑简化如下// Laravel侧生成桌台码 $tableId $request-input(table_id); $scene table_id . $tableId; $accessToken $this-getAccessToken(); // 缓存两小时 $response Http::post( https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token . $accessToken, [ scene $scene, page pages_customer/scan, width 430, env_version release ] ); return response($response-body())-header(Content-Type, image/png);小程序端onLoad时从options.scene里取出table_id转换成数字后请求订单接口。注意scene参数微信会做URL编码取出来后要decodeURIComponent一下否则遇到中文或特殊字符会被截断。6.3 高速并发下的重复下单问题开店初期出现过一次比较严重的bug同一桌顾客在13:02和13:03分别提交了两次订单但都显示的是“待确认”状态。原因是小程序端的“提交订单”按钮没有做防重复处理顾客手滑点了两下两个请求都通过了校验。解决办法有两层第一层前端在收到后端响应前禁用提交按钮这个大多数项目都会做但不够。第二层后端在orders表加了一个唯一索引索引字段是table_id和unique_order_no。按理说同一桌同时只能有一个“待确认”或“已确认”状态的非历史订单我在创建订单前先做一次select判断再加一个唯一索引兜底。如果并发请求同时到达数据库的唯一索引会拦住第二个请求Laravel捕获取QueryException后返回“订单已提交请勿重复操作”。6.4 真机调试时请求无法到达后端的排查这个坑在热搜词里也出现了很多人遇到。小程序开发者工具里接口请求正常但真机调试时就报错“request:fail”。我排查下来大部分原因是安卓手机设置了代理开发者工具能走代理真机上的代理配置不完整。开发版小程序没打开“不校验合法域名”选项但真机调试只有在开发版和体验版才能启用这个选项。手机系统和电脑不在同一局域网访问不到开发机上的本地接口。我的建议是调试阶段后端直接绑定内网IP手机和电脑连同一个WiFi小程序请求地址改成http://内网IP:8000同时勾选“不校验合法域名”。这样绕过所有域名问题专心调业务逻辑。等联调通过后再切换到HTTPS正式域名。6.5 小程序静音模式下播放提示音后厨端需要声音提醒。但在iOS设备上微信小程序在静音模式下即使调高了wx.setInnerAudioOption的volume系统静音状态依然会拦截声音。解决方案是给后厨的提示音做一个独立开关默认关掉系统声音控制改用WebAudio方式播放const ctx wx.createInnerAudioContext(); ctx.src /assets/audio/new_order.mp3; ctx.obeyMuteSwitch false; // iOS上绕过静音开关 ctx.play();obeyMuteSwitchfalse这个属性是iOS微信小程序播放音频绕过静音开关的关键。Android端默认不受静音开关影响但部分定制ROM会有例外所以最好在设置页加一个“测试声音”按钮方便现场调试不同品牌的设备。最后再补充一点我在双框架项目里的心得体会做完这个项目我对“框架选型”这件事有了新的理解。很多人喜欢争论ThinkPHP和Laravel哪个更好但在实际项目中真正决定成败的是团队能不能在既有代码的基础上快速交付。ThinkPHP的优点是中文文档全、上手快Laravel的优点是生态完整、队列和事件机制顺手。两个框架共存并不丢人只要你把数据库边界、接口契约、角色权限理清楚让它们各干各擅长的活项目的交付速度会比强行统一框架快得多。如果你想在这个项目基础上继续扩展我建议下一步引入一个简单的菜品估清功能后厨在制作者端一键置空某道菜前端所有桌台的购物车自动移除该菜品这个功能对门店的体验提升非常明显而且实现成本不高就是在菜品上下架逻辑上再加一个“今日估清”的Redis标记而已。
返回列表