资讯动态

Django餐饮系统实战:堂食外卖与骑手配送订单闭环设计

发布时间:2026/10/21 6:08:32 来源:尧图企业网站定制
做餐饮类的 Web 项目尤其是这种“堂食 外卖 骑手配送”三端打通的系统最怕的就是做到一半发现业务流程没理清代码写着写着各个模块之间互相打架。我拿到“Pythondjango的堂食餐厅外卖点餐骑手配送系统”这个标题的时候第一反应就是——这绝不是一个简单的 CRUD 练习而是一个把门店运营、用户下单、订单流转、骑手调度串起来的完整业务闭环。这种项目通常出现在毕业设计、课程设计或者中小型餐饮门店的数字化改造方案里它的核心价值不在于某个页面写得好看而在于订单状态怎么在各种角色之间顺畅流转。这个系统能做的事很明确顾客到店可以扫码看菜单、下单、结账顾客不在店可以通过外卖入口点餐、支付、等餐餐厅后台可以管理菜品、接收订单、操作出餐骑手端可以抢单、取餐、配送、完成订单。它解决的是传统餐饮门店里“前台点餐拥挤、外卖订单漏接、配送过程靠电话追问”这三大痛点。适合谁来参考如果你正在做 Django 相关的项目开发或者你想理解一个真实的商用业务系统是怎么按角色拆分功能的这篇拆解会给你一条可以照着走的路。先说一句实在话这种系统的难点不在单个功能有多复杂而在于订单状态在“用户 - 商家 - 骑手”之间流转时每一环的边界必须清晰。我下面就是从整体设计、数据建模、核心流程、第三方集成、性能安全、常见问题这几个角度把这个系统彻底拆开来讲。1. 项目拆解这套系统到底在解决什么问题要理解这个项目先别急着看代码把业务流程跑一遍。顾客到店坐下扫桌上的二维码手机里弹出餐厅的点餐页面选好菜、下单、支付后厨的接单界面立刻出现新订单菜品做完以后点击出餐此时如果订单是外卖骑手端会收到通知接单、到店取餐、配送、完成。整个链路里同一个订单在不同阶段出现在不同的用户界面里而所有环节都围绕着一个核心——订单。1.1 核心需求解析拆开标题里的三个关键词就明白需求边界了堂食点餐面向到店顾客。核心诉求是减少服务员点单工作量提升翻台效率。功能上需要扫码进入、菜品分类展示、购物车、桌台绑定、订单提交。比较关键的是“桌台”概念堂食订单必须知道顾客坐在哪一桌所以要在二维码里编码桌号信息。外卖点餐面向不在店里的顾客。核心诉求是让用户随时能下单并且知道订单状态。功能上需要用户注册登录、地址管理、在线支付、订单跟踪。外卖订单需要记录完整的收货地址和时间配送费的计算逻辑也和堂食不一样。骑手配送面向配送员。核心诉求是高效接单、取餐、送达。功能上需要骑手端订单列表、抢单/派单、取餐操作、送达确认。骑手可能需要同时处理多个订单订单列表要有合理的排序逻辑。这三块业务如果割裂开做各自都能跑通但一旦拼在一起就会出现很多隐蔽的问题。比如同样一个菜品堂食和外卖的库存是不是共享堂食用户提交订单以后后厨怎么知道这是 3 号桌的外卖订单支付成功之后骑手端怎么第一时间看到这些问题不是在某个模块内部解决的而是在订单模型和状态设计层面解决的。1.2 系统角色与权限设计用户角色是这类系统最先要定的东西。Django 自带的 User 模型只有 is_staff 和 is_superuser分不清“顾客、商家、骑手”所以需要扩展。我建议用 Profile 扩展方式做角色区分而不是直接改 User 表。原因很简单登录认证的还是同一套机制但不同角色看到的数据完全隔离。给 User 加一个 OneToOne 的 Profile 模型里面放 role 字段用 choices 限定为 CUSTOMER、RESTAURANT、COURIER 三种同时配合 Django 的 Group 权限把“管理菜品”“处理订单”“接单配送”这类操作权限分到对应组里。权限这边有个容易忽略的点接口级权限控制。前端页面隐藏按钮只是用户体验问题后端必须校验当前登录用户是否有操作权限。比如出餐操作只能由餐厅端用户调用骑手接单只能由骑手端用户调用这类校验写在 View 的 dispatch 方法里统一处理比在每个函数里重复判断要干净得多。1.3 技术选型为什么是 Django 而不是前后端分离现在很多人一上来就搞 Vue DRF 前后端分离我觉得要分场景。像这种包含后台管理、权限控制、事务处理的业务系统Django 的模板 ORM Admin 能大幅降低开发量。尤其对于课程设计或个人项目Django 的 ModelAdmin 可以直接生成餐厅后台的菜品管理界面省掉一大半工作量。但纯模板渲染也不是没有缺点。堂食扫码点餐和骑手端页面需要实时刷新订单状态如果全部靠刷新页面体验会很差。我做的方案是页面主体用 Django 模板渲染需要实时更新的局部区域用 AJAX 定时轮询。Django 的 JsonResponse 返回 JSON 数据前端每隔几秒请求一次接口效果已经足够流畅。当然如果你想用 WebSocket 实现真正的即时推送Django Channels 也是一个成熟方案后面的章节我会专门讲。2. 方案设计核心模块划分与数据表结构这个项目能不能站得住脚看数据模型就知道。用 Django 的思维来说Model 定义好了业务流程就清晰了。我按照业务边界把数据模型分成四组用户与权限、餐厅与菜品、订单中心、配送管理。2.1 用户、餐厅与菜品的数据结构餐厅和菜品是一对多的关系。餐厅表Restaurant需要字段名称、Logo、联系电话、营业时间、公告。菜品表Dish需要字段所属餐厅、名称、图片、描述、原价、售价、分类、是否上架、售罄标记。菜品分类建议单独建一个 Category 表因为分类是相对稳定的和餐厅多对一。餐饮项目里有个菜品属性的问题值得注意口味规格。比如“辣度选择微辣/中辣/特辣”“规格小份/大份”这需要在点餐时由用户选择直接影响订单明细的记录。我的做法是在 Dish 下面加一个 DishSpec 表记录规格名称和加价金额下单时用户选择了规格订单明细里就记录规格 ID 和实际成交价。如果一开始不考虑这个后面订单详情页就会缺失关键信息。这里还牵扯到库存问题。很多小餐饮店其实不管理精确库存但“售罄”这个状态必须有。设计上不需要在 Dish 表加库存数字字段一个 is_sold_out 布尔字段就够用了。如果确实要管库存就在 DishSpec 表上做数量控制但一般个人项目做到“售罄”这一步就够了。2.2 订单与购物车的模型设计思路购物车是临时数据最好设计成服务端存储别依赖浏览器 localStorage。因为用户可能在手机上点餐中途切换设备或者付款前刷新页面服务端购物车才能保证数据不丢。直接用 Django Session 存购物车也够用但我建议建一个 CartItem 模型字段包含用户、菜品、规格、数量、加入时间。好处是可以在后台直接看到当前有多少用户处于“选菜未下单”状态方便后续做营销分析。订单模型是整套系统的数据核心我建议拆成三张表Order订单主表订单号、用户、餐厅、订单类型堂食/外卖、桌号、收货地址、订单金额、配送费、实付金额、支付状态、订单状态、备注、下单时间。OrderItem订单明细表所属订单、菜品名称、规格、单价、数量、小计。订单明细为什么要冗余菜品名称和单价因为菜品价格和名称以后可能修改而历史订单必须保留下单那一刻的信息。OrderLog订单状态日志订单、操作人、动作、时间。这个表特别适合排查问题——用户说“我下单了但餐厅没收到”看一眼日志就知道卡在哪一步了。订单状态建议用整数常量 choices 枚举。常见的状态流待支付 - 已支付/待接单 - 餐厅接单 - 出餐中 - 待取餐/配送中 - 已完成另有已取消、退款中等状态。2.3 骑手调度与配送任务拆分配送模块不要和订单模块耦合得太紧。订单属于“交易域”配送属于“履约域”两者通过订单号关联但状态必须分开。我建议建一张 DeliveryTask 表所属订单、骑手、状态待接单/已接单/已取餐/已完成、取餐时间、送达时间、配送距离、配送费。为什么要把配送独立成表因为一个骑手可以同时配送多个订单顺路单如果把配送状态直接写在订单表上骑手管理多个订单时就要改多行数据逻辑会乱。拆开以后骑手端只需查 DeliveryTask 表任务状态一目了然。派单方式上还有个值得聊的点抢单还是派单抢单的逻辑简单骑手端拉取“待接单”列表先到先得派单则要有调度算法一般按距离和当前任务数分配。个人项目从抢单做起比较稳妥因为不需要考虑复杂的分配算法用户体验也更直接。等系统成熟了再往 DeliveryTask 加一个 assigned_by 字段做自动分单。3. 核心业务逻辑堂食、外卖、配送的关键代码实践模型建好以后真正的业务逻辑集中在 View 层。这一章我不贴完整代码只挑最关键的几个流程来讲清思路。3.1 堂食扫码点餐的完整流程堂食场景的灵魂在“扫码”这一步。餐厅每张桌子打印一个二维码二维码内容其实就是一个 URL路径是/table/{table_id}/。顾客扫了码带着table_id进入点餐页面选择菜品、加入购物车、提交订单。这一步要设计的重点是堂食订单不需要在线支付直接下单就到后厨。所以提交订单的接口要做两件事创建 Order 记录订单类型堂食桌号table_id支付状态不需支付同时推送消息给餐厅端。Django 的 post_save 信号很适合做这个——Order 创建后信号里触发 WebSocket 通知餐厅端界面就能实时弹窗。后厨接单环节要做一个状态变更餐厅点击“接单”时Order 状态从“待接单”变成“接单/制作中”。这个操作很简单但要注意加上select_for_update()锁行防止两个操作员同时点击同一订单导致状态覆盖。3.2 外卖订单的支付回调与状态流转外卖订单走的是在线支付。支付平台的通用交互方式是用户在页面点击支付后端生成支付链接用户跳转到支付平台完成支付平台后台向你的服务器发送异步通知。这里面最关键的代码实践是支付回调的幂等处理。支付平台可能会因为网络抖动对同一个订单发送多次回调如果你的回调逻辑是“收到回调就把订单改为已支付”第二次回调就会把已支付的订单再改一次如果这时候订单已经被取消就会造成不可恢复的错误。正确做法是回调处理前先查订单当前状态只有待支付状态才执行支付成功逻辑否则直接返回成功应答。用户支付成功后系统自动创建配送任务如果属于外卖订单。这里要注意配送任务的创建不要写在支付回调里串行执行可以用 Celery 异步任务回调里只做“改状态”这一个操作然后触发一个create_delivery_task.delay(order_id)这样回调响应速度快不容易引发支付平台超时重试。3.3 骑手接单与配送状态流转骑手端的订单列表需要一个“可抢单”列表查询条件是配送任务状态为待接单、餐厅已出餐、距离在一定范围内。查询时可以按距离排序但距离计算如果每次实时调用地图API计算太费资源我的方案是餐厅的经纬度和用户收货地址的经纬度都是静态数据创建配送任务时就把直线距离算好存进表里。列表页按这个字段排序就够了。骑手点击“接单”时同样要用事务处理。代码逻辑是with transaction.atomic(): task DeliveryTask.objects.select_for_update().get(idtask_id, statusPENDING) if not task: return JsonResponse({code: 1, msg: 手慢了订单已被抢}) task.courier request.user.profile task.status ACCEPTED task.save()select_for_update()在这里作用很大。两个人同时抢同一个订单事务锁保证只有一个能读到 PENDING 状态的任务并成功更新另一个人会得到空的查询结果从而走到“订单已被抢”的分支。骑行完成后的“确认送达”操作核心逻辑就是更新配送任务状态、更新订单状态为已完成。另外最好在这里记录完成时间这是骑手绩效计算的基础数据。4. 关键集成地图定位、消息推送与文件上传一个真实可用的系统离不开几个外部服务。我把它们单独列一章因为很多开发者在写完业务逻辑后发现集成时踩了一堆坑。4.1 地图与定位服务的接入方式堂食扫码不需要定位但外卖配送环节需要预估配送费和展示配送距离。最常见的做法是接入地图服务使用 Web 服务 API 完成三件事餐厅地址转经纬度地理编码用户填写收货地址后地址转经纬度并计算到餐厅的距离骑手端展示配送路径接入时注意距离计算分为“直线距离”和“骑行距离”。直线距离用经纬度直接算球面距离Haversine 公式适合排序骑行距离要请求地图 API 的路线规划适合展示给用户预估配送时间。两种距离建议分开存因为骑行距离计算有配额限制不能每次列表请求实时调用。4.2 WebSocket 做的实时订单提醒餐厅端和骑手端都需要实时接收新事件用 HTTP 轮询也能做但 WebSocket 是更优雅的方案。Django Channels 把 WebSocket 集成进 Django 项目比很多人想象中简单。架构上可以按角色分房间餐厅进入restaurant_{restaurant_id}房间骑手进入courier_{courier_id}房间。后端在订单创建、状态变更时用channel_layer.group_send发消息前端 WebSocket 收到消息后自动刷新订单列表或弹提示。集成时最容易被坑的是部署层。WebSocket 需要支持长连接传统的 WSGI 服务器比如 Gunicorn撑不住需要用 Daphne 或者 Uvicorn 配合 ASGI 模式启动。个人项目的话开发环境直接runserver就能支持部署到服务器上记得检查反向代理层面是否配置了 WebSocket 的升级请求转发。4.3 菜品图片上传处理菜品图片上传看起来很简单其实有很多隐患。表结构上只需要一个 ImageField但实际操作中要处理两个问题第一是图片尺寸控制。手机拍照的图片动辄几 MB如果直接把原图存进服务器前端加载菜品列表会非常卡。我的做法是用 Pillow 在上传时做压缩处理统一调整为 800x600 的 JPEG质量参数设置在 85 左右既能保持观感又能把体积控制在 100KB 以内。第二是文件存储策略。开发环境放本地 media 目录没问题部署到生产环境建议用对象存储服务。Django 只需要配置 DEFAULT_FILE_STORAGE代码里改一行配置就能切换一开始就按这种模式写后面迁移存储后端会很轻松。5. 性能优化与安全加固上线前必做的几件事个人开发和真实部署完全是两回事。以下这些问题我是在项目上线以后才真正理解的现在先给你排掉这些雷。5.1 Django ORM 查询性能优化菜品列表页是最容易产生 N1 查询的地方查询菜品列表然后循环每个菜品查它的图片和分类。Django 的 ORM 封装的select_related和prefetch_related就是解决这个问题的。一个典型的反例# 不要这么写 - 循环内查询数据库 dishes Dish.objects.filter(restaurantrestaurant_id) for dish in dishes: category_name dish.category.name # 每条记录都触发一次SQL正确写法dishes Dish.objects.select_related(category).filter(restaurantrestaurant_id)下单高峰期订单列表的查询也要注意分页。Django 的 Paginator 在数据量大时会执行 COUNT 查询这个没问题但要避免在订单列表页查询所有关联的 OrderItem 和 OrderLog可以用Prefetch对象只取真正要展示的字段或者直接用values()只查需要的字段。5.2 并发场景下的事务与锁餐饮系统是天然的高并发场景。同一个菜品可能同时被多个人下单同一个骑手任务可能被多个骑手同时抢。如果不用事务和锁就会碰到库存超卖、任务被重复认领的问题。建议统一遵循三条规则对订单、配送任务这类“状态变更类”操作用transaction.atomic()包裹并在查询时加select_for_update()。支付回调这类第三方触发操作做成幂等接口同一笔请求处理多次结果一致。库存扣减操作用F()表达式做原子更新。比如Dish.objects.filter(id1, stock__gte1).update(stockF(stock) - 1)这条语句是原子的查询和更新在一条 SQL 里完成不会出现超卖。5.3 防越权与数据隔离这类多角色系统最怕的就是用户越权访问别人的数据。Django 的 ORM 层很容易写漏过滤条件导致用户 A 查到了用户 B 的订单。我踩过一次这样的坑某次测试时发现一个 ID 为 101 的订单换个用户登录也能访问。原因就是视图里直接Order.objects.get(idorder_id)没有带用户条件。修复方案是给所有涉及用户的查询统一加上过滤。更保险的做法是写一个查询帮助函数强制注入当前用户 IDdef get_user_order(user_id, order_id): order Order.objects.filter(idorder_id, user_iduser_id).first() if not order: raise PermissionDenied return order对外卖订单和配送任务的查询都要按照“当前身份只能读写自己的数据”的原则来做然后在列表页、详情页、变更操作的入口处统一校验。这个底线守不住其他安全措施都是空谈。6. 常见问题实录与调试建议这个项目的开发周期里我发现有几个问题是几乎每个做过同类项目的人都会遇到的这里挑典型的来复盘。6.1 订单状态不一致支付成功但订单还是待支付排查这个问题的思路很固定。先看支付回调日志有没有进来如果没进来多半是回调地址配置错误或者测试环境没有内网穿透如果回调进来了但订单状态没变检查回调处理函数前面的幂等判断逻辑很可能订单状态已经先被人工修改了。调试阶段建议在 OrderLog 上完整记录每次状态变化的来龙去脉带上了操作来源排查速度会快很多。6.2 配送任务重复创建我遇到过支付回调后配送任务被创建了两次的情况。原因是回调重试和前端手动查询接口并发触发。解决办法有两个一是给配送任务表加 unique 约束订单 ID 唯一数据库层面做兜底二是在创建前用get_or_create替代create从逻辑层面保证同一个订单只会有一个配送任务。6.3 扫码进店后 session 丢失下单失败浏览器对第三方 Cookie 的限制越来越严扫码打开的页面如果和服务端不在同源环境Django 的 CSRF 校验就会拦截 POST 请求。最开始测试时这个坑非常隐蔽。排查方案是把扫码的点餐页面放进 Django 自己的域名下扫码只是携带桌号参数跳转到站内页面或者在后端配置里对某些公开接口放开 CSRF 校验。线上部署一定要用 HTTPS不然支付和登录环节会被拦截。6.4 时区问题导致订单日期错乱Django 默认的 TIME_ZONE 是 UTC如果你的服务器在海外或使用云服务且没设置时区用户下单时间和骑手完成时间会差好几个小时。务必在 settings.py 里设置TIME_ZONE Asia/Shanghai并开启USE_TZ False或者在模型里统一用django.utils.timezone.now()存 UTC 时间、前端展示时做本地化转换。餐饮系统的营业时间判断、订单统计报表都依赖准确的时间这个问题越早解决越好。7. 部署落地的补充经验代码写完只是第一步真正用起来还有几个点值得提一下。静态文件和媒体文件在 Django 部署时最容易被忽略。Debug 模式下 Django 能自己处理静态文件但关掉 Debug 后就必须用反向代理指向静态目录。菜品图片的存放路径要确认 Web 服务器能读必要时用collectstatic把后台静态资源收集到统一目录。数据库建议用 PostgreSQL。Django 对 SQLite 和 MySQL 都支持但生产环境更推荐 PostgreSQL。它的事务支持更完整对select_for_update()等行级锁的处理也更稳定。开发环境用 SQLite 没问题但别到上线才从 SQLite 迁到 PostgreSQL到那时数据迁移会遇到不少小麻烦。日志系统要提前配好。至少把 Django 的 error 日志、支付回调的访问日志、骑手操作的业务日志分开记录这样出问题能快速定位到是哪个环节。很多项目上线以后才意识到日志的重要性那时再补很被动。我个人在实际开发中的体会是这个系统的开发节奏应该定为“业务流程优先”。先把堂食下单、外卖支付、骑手配送这样的一整条核心链路打通再去完善后台管理、数据统计、营销工具这些外围功能。很多时候我们把精力花在了一些边缘功能上到最后核心流程反而留有隐患。每当有人问我这类系统怎么做时我都会强调同一句话无论功能列表多长订单状态流转的稳定性和角色权限的边界永远是你最先该夯实的地基。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑