恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

基于Python+Django的餐厅外卖点餐配送系统设计与实现

  • 首页
  • 资讯中心
  • /
  • 基于Python+Django的餐厅外卖点餐配送系统设计与实现

相关资讯

书法AI识别:从OCR到笔势建模的范式突破 2026/10/21 19:24:20
技术图绘制指南:从读者分析到文本化设计 2026/10/21 19:24:20
LiteLLM实战:统一大模型接入与网关代理的架构笔记 2026/10/21 19:24:20

最新资讯

CentOS 7手动安装网卡驱动全流程:从硬件识别到编译排错
Elasticsearch+Kibana实战:从部署到C++封装的搜索引擎开发指南
3D-Diffusion:用扩散模型实现可扩展的3D资产生成
CC/Codex自动实证训练营复盘:两天在真实项目中跑通AI编码智能体
Elasticsearch查询实战:从倒排索引到聚合优化全解析
Git 修改用户名与重置账号密码:配置、历史重写与凭据清理

今日推荐

SAP 屏幕上的金额未必存在数据库表里,从 WB2R_SV 的 Net Amount 看懂运行时计算
具身智能中‘指哪打哪’的视觉语言动作联合建模
AI时代人工翻译技术博客的价值与实践指南

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

基于Python+Django的餐厅外卖点餐配送系统设计与实现

发布时间:2026/10/21 19:29:21
基于Python+Django的餐厅外卖点餐配送系统设计与实现 一个项目如果能把“顾客点餐、商家接单、骑手配送”这条链路跑通基本上后端开发该碰到的核心问题就都碰了一遍。最近我花了两周时间把一个基于 Python Django 的堂食餐厅与外卖点餐配送系统完整地搭了出来项目代号就叫 43lq510m。这篇文章不是从零开始的教学文档而是把我做这个系统时踩过的坑、想清楚的逻辑、写完觉得值得留下的模块设计全部整理出来。无论你是准备拿这类题目做课程设计、写毕业项目还是单纯想练手一个完整的全栈业务系统这篇文章都应该能让你少走一些弯路。这个系统覆盖的场景并不复杂但业务链路很长顾客在手机端或餐厅内扫码点餐可以选到店自取也可以选外卖配送餐厅商家在后台管理菜品、处理订单骑手端负责抢单、取餐、送达中间还有订单状态流转、支付回调、超时关闭、配送轨迹记录这些细节。说直白一点这就是一个没有大厂资源但五脏俱全的外卖系统非常适合用来理解真实业务系统是怎么分层、怎么设计的。1. 项目到底要解决什么问题动工之前我先把需求从头到尾拆了一遍。很多人写这类系统容易一开始就扑到代码上结果做到一半发现订单状态乱、角色权限分不清、数据改起来牵一发动全身。所以第一步不是写 Models而是把业务链路和角色边界画清楚。1.1 业务链路梳理这个系统的核心链路其实只有一条顾客下单 → 商家接单 → 后厨制作 → 骑手取餐配送 → 顾客确认收货。但堂食场景会稍微特殊一点。顾客在餐厅内扫码点餐订单虽然也是通过系统提交但不需要骑手配送出餐后由餐厅服务员或顾客自己取餐。也就是说同一个购物车和订单模型要同时支持两种履约方式外卖配送和到店自取。我用一个字段order_type来区分这两种模式取值是takeout外卖和dinein堂食。这比建两套订单表要合理得多因为不管是哪种类型订单的基础属性——商品明细、金额、支付状态——都是一样的只是履约环节不同。整个流程里的状态节点主要有这些待支付顾客已提交订单但还没完成支付已支付/待接单支付成功商家端出现新订单制作中商家确认接单后厨开始做待取餐/配送中餐品做好骑手取餐后开始配送已完成顾客确认收货或系统自动完成已取消/已退款用户取消、商家拒单或支付超时。我把这一整套状态流转放到了订单模型里并用一个status字段统一管理。这样前端展示、后端接口、数据统计都只需要关注一个字段不会出现状态散落各地、对不上的问题。1.2 三种角色的边界划分系统涉及三类使用者顾客、商家、骑手。如果再加上后台管理员其实是四类。我在 Django 的 User 模型上做了扩展用user_type字段区分角色。顾客端主要关心的是菜单浏览、下单支付、订单列表、评价晒单。商家端关心的是菜品管理、订单处理、营业统计。骑手端最核心的需求是抢单、取餐、送达确认。在设计角色权限的时候我意识到一个关键点虽然在同一个系统里但三个角色的接口和页面是基本隔离的。Django 本身有 Group 和 Permission 机制但在这个规模的项目里用user_type字段配合装饰器做权限控制反而更直接。我给视图写了一个简单的require_role(merchant)装饰器不够通用但够用。1.3 为什么选 Python Django选型这件事我认为比写代码本身更值得说。Python Django 对这个场景非常契合原因有三个。第一Django 自带 Admin 后台和 ORM能把“管理系统”这一类工作的开发成本压到极低。商家端的菜品管理、订单管理等页面很大一部分逻辑可以通过 Django Admin 或者基于 Admin 风格定制来实现省掉了大量重复的 CRUD。第二Django 的 ORM 在处理订单、商品、购物车这类强关联数据时表达能力非常顺。尤其是我需要做“下单时锁定库存”“订单完成后回写销量”这类操作时ORM 配合事务管理比手写 SQL 要清晰得多。第三生态成熟踩坑成本低。支付接入、短信验证码、JWT 登录、WebSocket 实时推送这些都有非常成熟的第三方库不需要从零造轮子。对于个人开发者做这种中小型项目来说这是最可持续的技术路线。2. 数据库设计与 Model 的取舍这个项目的数据模型我前前后后改了四版。第一版就是照抄网上常见的“用户表商品表订单表”三板斧后来发现根本不够用。不是表数量的问题而是字段设计能不能支撑真实业务流转。2.1 核心表结构拆解我在这个项目里主要设计了以下几张核心表每张表都有一些值得单独说明的设计点。User 用户表扩展了 DjangosAbstractUser新增user_type字段分别标记顾客、商家、骑手账号。另外加了phone字段作为登录名之一因为实际场景里顾客很少会记用户名手机号才是真正的入口。Restaurant 餐厅表这里存的是餐厅名称、地址、公告、营业状态等基础信息。堂食和外卖的订单都需要关联到餐厅所以这不是一个可选项而是所有业务数据的数据根。Dish 菜品表属于某个餐厅包含名称、图片、价格、月售、库存、分类、是否上架。我单独把库存字段拉出来是为了在顾客下单时能做库存校验。真正专业的系统也许会用独立的库存服务但对这个体量来说一个字段足够了。Cart 和 CartItem 购物车表Cart 绑定用户和餐厅。CartItem 关联菜品和数量。我原本考虑过不建购物车表、直接存 localStorage但后来发现顾客在手机上点餐如果换设备或者刷新页面购物车就没了体验会很差所以最终还是把购物车放到了服务端。Order 和 OrderItem 订单表Order 记录订单总金额、支付状态、配送地址、订单来源堂食/外卖、下单时间等OrderItem 记录每道菜在某个订单中的快照信息。这里必须注意OrderItem 里要有菜品的快照字段包括菜品名称、价格、数量而不是只存一个外键到 Dish。因为商家改价或者下架菜品后历史订单里的金额不能被影响。Delivery 配送表外卖订单会生成一条配送记录关联订单、骑手、取餐地址、送达地址、配送状态。这样配送链路的数据不会跟订单主表揉在一起后续扩展配送范围、骑手轨迹都会方便很多。2.2 订单状态机设计状态机是这个系统里最容易写乱的地方。我见过有人把订单状态用 10 个 if else 散落在视图函数里维护起来极其痛苦。我建议把所有状态定义成常量并且提前写明每个状态“允许进入的下一个状态”是什么。比如待支付可以进入已支付也可以进入已取消已支付等待接单可以进入制作中也可以进入退款中配送中只能进入已完成除非发生异常进入退款。这样在设计接口时后端只需要判断“当前状态在不在允许的状态转移集合里”而不是到处散落状态判断。代码看起来更干净也更容易做单元测试。我自己的做法是写了一个OrderStateMachine类封装了状态定义和can_transition方法。每个 view 在更新订单状态之前先调用它校验不合法的直接抛异常。2.3 一个值得细说的设计购物车与订单快照做这套系统的时候有件事让我纠结了一段时间购物车里的菜品数据和订单里的菜品数据到底要不要用同一套表结构购物车是“可变的、临时的、以用户为中心的”订单是“不可变的、永久的、以交易为中心的”。这两者的数据生命周期完全不同。最终我决定用两张表承载CartItem 里的数量可以随便改一旦生成 Order 和 OrderItem购物车里的数据就被清空订单数据成为历史快照。这也引出一个经验凡是涉及交易的数据一定要做快照不要依赖实时查询。商家改了菜品价格、删了菜品、改了库存都不应该影响已经生成的订单金额和明细。3. 核心功能的实现路线3.1 项目初始化与基础配置技术栈方面我用了 Django 4.2 Django REST Framework SimpleJWT 做认证数据库用 PostgreSQL缓存用 Redis实时推送用了 Channels 和 WebSocket。项目初始化这一步看起来简单实际上有很多细节会影响后面的开发效率。首先是 Settings 的拆分我把配置拆成了 base.py、dev.py、prod.py 三个文件这样本地开发和部署上线可以用不同的数据库、不同的 DEBUG 设置不会出现本地跑得好好的、线上怎么都不对的情况。然后是 CORS 配置。由于前端是独立的 Vue 项目和后端分开部署所以跨域问题一定要提前处理好。我用django-cors-headers配置好CORS_ALLOWED_ORIGINS把前端的地址加进去。数据库选型我不建议用 SQLite哪怕只是为了学习也不建议。订单系统的数据关系复杂SQLite 在并发写入和类型支持上都会有一些限制。装一个 PostgreSQL 或者 MySQL用 Docker 起一个容器非常方便。3.2 下单的事务与并发控制顾客点击“提交订单”后端要干的事情比看上去多得多校验购物车、计算金额、生成订单、扣减库存、清空购物车。这些操作必须在一个事务里完成任何一个环节失败所有操作都要回滚。Django 的transaction.atomic()在这里就派上了用场。另外高并发下扣库存必须用select_for_update()锁定菜品行否则会出现多个订单同时通过库存校验、实际库存被扣成负数的问题。下面是我下单核心逻辑的一个简化版本from django.db import transaction transaction.atomic def create_order(user, cart, order_type, addressNone): cart_items cart.items.select_for_update().select_related(dish) order_total 0 order Order.objects.create( useruser, restaurantcart.restaurant, order_typeorder_type, statuspending_payment, addressaddress, ) for item in cart_items: dish item.dish if dish.stock item.quantity: raise ValueError(f菜品 {dish.name} 库存不足) dish.stock - item.quantity dish.save(update_fields[stock]) OrderItem.objects.create( orderorder, dishdish, dish_namedish.name, dish_pricedish.price, quantityitem.quantity, subtotaldish.price * item.quantity, ) order_total dish.price * item.quantity order.total_amount order_total order.save(update_fields[total_amount, status]) cart.delete() return order这里有几个细节需要说明。第一select_for_update()必须放在事务里才有效否则数据库不会真的加锁。第二扣库存时用update_fields限定只更新stock字段避免 ORM 把所有字段都 UPDATE 一遍减少数据库的写压力。第三OrderItem 里存的是菜品名称和价格的快照而不是直接引用 Dish 对象这是为了历史订单的稳定性。3.3 骑手抢单与实时送达状态骑手端最核心的功能是抢单和配送状态回传。抢单的本质是“多个骑手同时对一单进行竞争”在数据库层面最容易出现的问题是多个骑手同时更新同一个订单导致一单被多个骑手接走。解决这个问题我用的方法是条件更新只更新“当前骑手为空且状态匹配”的行。Django ORM 里用filter加条件更新返回受影响的行数如果为 0 说明被别人抢了updated Delivery.objects.filter( iddelivery_id, rider__isnullTrue, statuspending_rider ).update(riderrequest.user, statusaccepted) if updated 0: raise ValueError(订单已被其他骑手接走)这个方案比先查后更安全因为后一步的更新条件会校验前一步的结果不会出现脏读。配送状态回传我用的是 WebSocket。骑手在 App 端点击“已取餐”“已送达”前端把经度纬度一起传过来后端通过 Channels 推送给顾客端和商家端。实现上我用了一个delivery_status的 Group骑手更新状态后往这个 Group 里发消息所有关注这个订单的前端都能收到实时状态。async def notify_delivery_status(order_id, status): await channel_layer.group_send( forder_{order_id}, { type: delivery_status.message, status: status, } )这里要注意的是WebSocket 连接需要鉴权。我用的是 JWT前端在连接 URL 上带 token后端通过AuthMiddlewareStack解析用户身份再判断该用户是否和这个订单有关联防止订单信息泄露给别人。3.4 商家端订单处理与数据看板商家端的功能主要集中在 Django Admin 和自定义的商家中心页面上。商家可以处理订单接单、开始制作、出餐、完成订单。我重新定制了 Admin 的 list 页面用list_display展示订单号、金额、状态、下单时间加了自定义按钮来执行接单、出餐操作操作本身也是走状态机校验。商家数据看板是这套系统里比较容易体现价值的功能。我统计了三个核心指标今日营业额、今日订单数、今日热门菜品。实现方式很简单在 Order 上做聚合查询today timezone.now().date() revenue Order.objects.filter( created_at__datetoday, status__in[completed, delivering] ).aggregate(totalSum(total_amount))如果订单量大了可以再加一层缓存比如把今日营业额缓存到 Redis订单完成时更新缓存不需要每次都全表聚合。小项目阶段这么做纯属过度设计但思路可以留着以后量大了直接套用。4. 开发过程中踩过的坑以及排查建议这里我整理了开发过程中遇到的最典型的几个问题和解决过程每一个都值得你看一眼。4.1 支付回调的幂等性问题我接入的是某个第三方支付平台的沙箱环境。支付成功后平台会回调通知后端接口把订单标记为已支付。这个回调可能因为网络问题多次发送如果没有幂等处理就会出现订单状态被重复更新、甚至金额被计算两次的问题。解决方式非常朴素在支付回调入口先检查订单当前状态。如果已经是已支付直接返回成功不再执行后续逻辑。在此基础上我还加了一个payment_notification表记录每次回调的请求指纹重复的回调直接忽略避免极端情况下的重复处理。4.2 超时订单关闭用户提交订单但迟迟不付款这类订单如果一直挂着会占用菜品库存。我用的是定时任务方案用一个独立的 Django management command每分钟扫描一次超过 15 分钟未支付的订单将其标记为过期关闭同时把库存加回来。class Command(BaseCommand): def handle(self, *args, **options): expired_time timezone.now() - timedelta(minutes15) expired_orders Order.objects.filter( statuspending_payment, created_at__ltexpired_time ) for order in expired_orders: with transaction.atomic(): order.status cancelled order.save(update_fields[status]) restore_stock(order)这个命令用 cron 每分钟跑一次就可以不依赖 Celery 这种重型组件。但如果你后续要给系统加通知功能比如超时提醒用户建议尽早引入 Celery Redis任务队列能做的事情远比定时脚本多得多。4.3 前端页面在移动端的适配顾客端主要是在手机上用的我一开始用了桌面端的布局思路结果在手机上效果惨不忍睹。后面改成移动优先的响应式布局用断点把菜单列表、购物车侧滑栏、订单卡片重新排了一遍。如果你不擅长写复杂的 CSS建议直接用 Bootstrap 5 或者 Tailwind CSS先把移动端的显示效果调好。菜单列表用卡片式布局菜品图片比例固定 1:1价格和加号按钮放在右上角拇指操作比较顺手。4.4 一个小细节并发抢单的测试方法我写完骑手抢单接口后用并发工具模拟了 10 个骑手同时抢同一单。跑了几轮之后发现有时候成功了 2 个、有时候成功了 3 个问题的根源就是没有用条件更新。后来改成filter(...).update(...)之后并发测试稳定在一个成功。测试这类接口的并发问题不要靠人肉多点几次用脚本或者测试工具并发请求几十次很快就知道自己的代码有没有竞态条件。5. 部署上线时的几个实用建议系统开发完成后部署到服务器又是一轮新问题。这里有几个我在上线过程中总结出来、以后也一定会遵守的注意事项。5.1 静态文件和媒体文件服务Django 默认不擅长服务静态文件生产环境一定要交给 Nginx 或者对象存储。菜品图片是典型的媒体文件我放在了专门的/media/路径由 Nginx 直接提供访问。配置好 Nginx 的反向代理之后/static/和/media/都不需要经过 Django 处理后端压力小很多。5.2 MySQL 字符集与数据库配置如果你的数据库用 MySQL建库的时候一定要明确设置字符集为utf8mb4否则菜品名里的生僻字、Emoji 表情都可能乱码。表面上看起来是小问题等线上数据攒到一定程度再改字符集就非常痛苦了。5.3 Django Settings 里需要压的配置生产环境的DEBUG必须设为 False这个不用多说。另外ALLOWED_HOSTS要填服务器的域名和 IP不能是通配符。SECURE_SSL_REDIRECT如果已经上了 HTTPS 也可以放 True强制跳转。还有一点是CSRF_TRUSTED_ORIGINS如果用了 HTTPS 域名这个也要配好否则登录接口容易被 CSRF 校验挡下来。5.4 上线前必须做的演练真正上线前我习惯用生产环境配置在本地/服务器上完整地跑一遍全链路顾客注册、点餐、支付、商家接单、骑手配送、完成订单。这听起来很简单但常常能发现本地开发时一定不会暴露的问题回调地址没改成线上域名、静态路径拼错、Redis 密码没配、时区不对导致统计差了 8 个小时……这些问题每发现一个就能省下用户帮你踩雷的时间。6. 常见问题速查表问题 | 症状 | 排查方向 | 解决方案 登录后接口返回权限错误 | 顾客能访问商家接口 | 装饰器校验失败 | 检查user_type的赋值时机确认 JWT token 里的用户信息是否为最新 下单后库存没变 | 订单创建成功但没有扣库存 | 事务未包含扣库存代码 | 把扣库存放进同一个transaction.atomic()支付回调重复触发 | 订单被重复标记已支付 | 缺少幂等逻辑 | 通过订单状态和回调记录双重校验 骑手抢单重复成功 | 同一订单被多个骑手接走 | 没用条件更新 | 改用filter().update()原子操作 菜品图片无法访问 | 页面图片 404 | 媒体文件未对外开放 | 配置 Nginx 的/media/路由 顾客端页面在手机上显示错乱 | 布局挤压、按钮太小 | 未做移动端适配 | 使用响应式框架并调整断点这个项目做到最后我最大的体会是像这种业务链路完整的系统价值不在于功能多炫而在于把状态流转、数据一致性和角色权限这三件事想明白。只要这三条线是清晰的后面加功能、改页面、接支付都只是工作量的问题。反过来说这三条线理不清再怎么堆功能都容易翻车。如果你准备自己从零写一个类似的系统我建议的路线是先只做订单主链路不做骑手端把顾客点餐、商家接单跑通再逐步加上支付回调、配送、评价这类外围功能。这样一个模块一个模块地加每一步都能验证排查问题时也会轻松很多。最后再补一个小技巧——开发阶段一定多用真实手机访问测试别只盯着电脑浏览器看效果移动端的真实体验只有拿到手机上才能发现问题。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号