资讯动态

Python+Vue外卖点餐平台开发:从Django/Flask选型到订单并发控制实践

发布时间:2026/10/16 12:30:09 来源:尧图企业网站定制
1. 外卖平台的骨架先搞懂三个角色和一条订单主线先说个结论如果你正打算做PythonVue的美食外卖点餐平台又卡在Django和Flask之间不知道怎么选这篇文章也许能省你一周的纠结时间。作为练手或毕设项目外卖平台确实是个好选题——角色多、业务线长、权限复杂、数据关系完整做完之后前端、后端、数据库、部署全链路都过了一遍。但它也是典型的“看起来简单、做起来容易翻车”的项目翻车点往往不在某个技术难点而是订单状态梳理不清、角色权限混乱、抢单并发没处理。这套系统的业务主线其实只有一条用户浏览商家、加购物车、下单支付商家接单并出餐骑手抢单后到店取餐再送到用户手上。平台方负责商家入驻审核、骑手注册审核、订单监控和基础数据统计。只要抓住这条主线后面的代码结构就不会散。这里要特别强调一下“三个角色一个平台”的划分。很多同学做这个项目时只做了用户端和商家端骑手端草草放个列表页面结果整条订单链路走到“商家接单”就断了。我建议把骑手端当成和商家端同等重要的一等公民来设计因为外卖业务里最出彩的部分恰恰是骑手抢单和配送状态流转这也是答辩或面试时最容易被追问的点。角色定了之后下一步就是订单状态机。我见过不少项目把订单状态存成“待支付、进行中、已完成”这样过于粗糙的枚举后面一旦要加“退款”“超时取消”“骑手转单”就完全卡死。哪怕你的Demo只需要支持正常流程也要先把状态字段设计成可扩展的状态机。一个相对完整的状态序列大概是这样的待支付用户创建订单但还没有完成支付已支付待接单支付成功平台等待商家确认商家已接单制作中商家点击接单待取餐商家出餐完成等待骑手到店取餐配送中骑手取餐成功正在送往用户已完成用户签收订单结束已取消用户取消或商家拒单订单终止每个状态只能由特定角色触发。比如商家可以把自己接单后的订单改成“待取餐”但绝对不应该有权限把订单改成“配送中”那是骑手的操作。这个权限和状态绑定的设计就是整个后端权限系统真正要做的事情。我建议在写任何一行代码之前先把状态迁移表列出来表里至少包含当前状态、目标状态、触发动作、执行角色。把这张表贴到项目文档里后面写接口时几乎不需要再动脑子。2. 技术选型Django和Flask不是二选一而是分工不同很多人在“python基于vue的美食外卖点餐平台”这个标题里看到Django和Flask同时出现第一反应是“到底选哪个”。我在搭这个项目时实际采用的是“一主一辅”的混合方案主业务接口全部用Django写毕竟订单、商家、用户、骑手这一套CRUD和权限体系Django的ORM、自带Admin后台和中间件机制能省掉大量重复劳动另外单独用Flask起了一个很小的服务只跑订单超时提醒和简易统计任务因为这类轻量任务不需要Django全家桶。先看看这两个框架在项目里到底差在哪。下面的对比基于我做这个项目时的真实体会维度DjangoFlaskORM内置模型迁移、查询都很完善默认没有正式ORM要自己接SQLAlchemyAdmin后台自带admin配置一下就能管理数据需要额外引入Flask-Admin等扩展用户认证自带User体系和权限框架要自己组装JWT或Session方案适合场景业务重、角色多、模型关系复杂的单体系统轻量API、微服务、小工具对外卖平台这类多角色业务系统Django明显更合适。它的ORM在处理订单、菜品、购物车之间的外键关系时非常顺手Django自带的admin后台又能让你在起步阶段零成本获得一个平台管理后台去审核商家资质、查看订单流水。Flask当然也能做但同样的功能你需要多接好几个库学习成本反而上去。再说Vue。前端这边Vue负责任的是三套界面用户端浏览商家菜单、购物车、下单页、订单追踪、商家端店铺管理、菜品管理、订单处理、骑手端可抢订单列表、配送任务。三套界面交互逻辑差异很大我建议在项目初期就拆成三个独立入口。如果你按“一个Vue单页应用角色判断切换页面”来做后期组件打包、路由守卫、权限隔离都会很别扭。实际的工程目录我当时是这样组织的takeout/ ├── backend_django/ # Django主工程 │ ├── apps/ │ │ ├── users/ # 用户、商家、骑手账号体系 │ │ ├── merchant/ # 商家店铺与菜品管理 │ │ ├── order/ # 订单与购物车 │ │ └── rider/ # 骑手配送 │ └── takeout/ ├── notify_service/ # Flask轻量服务 │ ├── task_timeout.py # 超时扫描 │ └── stats.py # 简单统计 ├── frontend/ │ ├── user_web/ # Vue用户端 │ ├── shop_web/ # Vue商家端 │ └── rider_web/ # Vue骑手端 └── scripts/ └── fake_data.py # 演示数据生成用PyCharm打开这个目录后好处是所有代码都在一个项目窗口里管理后端、Flask服务、Vue前端都能直接看。尤其是Debug时从Vue前端发起的请求可以一路从浏览器Network追踪到Django断点非常直观。3. 后端最容易翻车的三块订单表设计、抢单并发、状态流转3.1 订单表设计一张表吃透所有角色订单是整个外卖平台的“总线”用户、商家、骑手各自的操作最后都会落在这张表上。字段设计直接决定了后续接口好写还是难写。我建议订单表至少包含这些核心字段class Order(models.Model): STATUS_CHOICES [ (UNPAID, 待支付), (PAID, 已支付待接单), (ACCEPTED, 商家已接单), (READY, 待取餐), (DELIVERING, 配送中), (FINISHED, 已完成), (CANCELLED, 已取消), ] order_no models.CharField(max_length32, uniqueTrue) user models.ForeignKey(User, on_deletemodels.PROTECT) shop models.ForeignKey(Shop, on_deletemodels.PROTECT) rider models.ForeignKey(User, nullTrue, blankTrue, on_deletemodels.SET_NULL) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultUNPAID) items_snapshot models.JSONField() total_amount models.DecimalField(max_digits10, decimal_places2) address models.CharField(max_length255) remark models.CharField(max_length255, blankTrue) create_time models.DateTimeField(auto_now_addTrue) pay_time models.DateTimeField(nullTrue, blankTrue) accept_time models.DateTimeField(nullTrue, blankTrue) finish_time models.DateTimeField(nullTrue, blankTrue)几个容易被忽略的设计点这里展开说订单号不要用自增主键要生成一个独立的业务单号。因为自增主键会暴露平台当天订单量而且多表关联时不方便传播。可以用时间戳加随机数生成比如20250407153012 6位随机数。商品信息保存为快照。有些同学会把订单和菜品做成一对多关联表这样其实很麻烦——商家改菜品价格后历史订单也会跟着变。正确的做法是下单时把当时的菜品名称、价格、数量整个存成JSON快照订单页面直接读快照展示。既保真又省一次联表查询。骑手字段用外键但允许为空。因为订单刚创建时还没骑手抢单如果字段设置为非空整个创建订单的动作会非常别扭。同时注意on_delete参数骑手注销时不要把订单删掉应该保留订单并把骑手字段置空。状态时间字段分开记录。很多同学只存一个update_time最后做超时统计或订单流转分析时才发现不知道商家几点接的单、骑手几点取到餐。我在做超时取消服务时就是因为有了这些独立时间字段才能一条SQL找出所有“支付超过30分钟还没接单”的订单。3.2 骑手抢单的并发控制先到先得不是读出来再改骑手抢单是整个项目里最值得拿出来讲的点。常规写法是“先查询订单状态判断是待抢单再更新骑手字段”但逻辑上存在一个经典的并发漏洞两个骑手同时读到订单状态为待抢单两个人都认为抢单成功最后只有一个更新生效另一个却不知道。我实际测试时用两个浏览器窗口模拟骑手同时点击抢单这个Bug必现而且不是偶现。问题出在读和写之间没有加约束。正确的做法是直接用条件更新让数据库来保证原子性from django.db import transaction from django.db.models import Q with transaction.atomic(): updated Order.objects.filter( idorder_id, statusPAID, rider__isnullTrue ).update(riderrider, statusDELIVERING) if updated 0: raise RiderTakeOrderError(订单已被抢走或状态已变化)这个写法的关键是update语句里的WHERE条件它在数据库层面把“读状态”和“写骑手”合并成了一个原子操作不会出现两个骑手都通过判断的情况。updated返回值是受影响行数只有等于1时才代表抢单成功。这一招在商家接单、超时取消等场景里同样适用。另外要提一下事务级别。Django默认的事务隔离级别在MySQL下通常是可重复读上面的写法在InnoDB下不需要额外加锁也能保证安全。如果实在担心并发高可以再加上select_for_update但条件更新已经够用而且代码更简洁。3.3 商家出餐与骑手送达用接口动作驱动状态流转订单状态不能“直接改库”。外卖平台的订单流转不建议写一个“修改状态”的通用接口那样整个状态机就失控了。更好的方式是为每个业务动作单独定义一个接口接口内部校验角色和当前状态再统一更新状态。例如商家端的两个关键接口# 商家接单 def accept_order(request, order_id): if not request.user.shop: raise PermissionDenied(商家才能接单) updated Order.objects.filter( idorder_id, shoprequest.user.shop, statusPAID ).update(statusACCEPTED, accept_timetimezone.now()) # 判断 updated 0 则返回“订单状态已变化”# 商家出餐完成 def mark_ready(request, order_id): if not request.user.shop: raise PermissionDenied(商家才能操作) updated Order.objects.filter( idorder_id, shoprequest.user.shop, statusACCEPTED ).update(statusREADY)骑手送达的逻辑类似骑手只能把“配送中”的订单改成“已完成”。这里有一个我在答辩时被追问过的细节商家接单后骑手端怎么知道订单变成了“待取餐”最省力的方案是前端定时轮询接口每5秒拉一次可抢订单列表如果想做得更专业可以引入WebSocket或SSE推送。但对于毕业设计和个人项目轮询是最可控的方案下面第4章会详细说怎么设计轮询才不卡。4. Vue端的联调登录态、路由守卫与订单轮询4.1 一套代码跑三个端还是拆三个工程这个问题我纠结了很久最后选择了拆成三个Vue工程。原因有两方面。一是部署和打包隔离用户端、商家端、骑手端的页面入口和静态资源完全独立各自build后放在不同目录后端通过不同路径访问。二是权限边界清晰商家端代码里完全不用出现骑手的页面和接口避免前端路由泄露。当然拆三个工程的代价是组件复用率变低比如订单卡片、商品列表这些样式相似的组件要复制三份。我的处理方式是复制第一份后把公共的列表样式、时间格式化函数抽成一个共享的本地包。整体工作量其实没有增加太多因为三端的交互差异本来就大硬塞到一个工程里反而要写一堆条件分支。如果时间特别紧也可以做一个包含三个角色入口的单工程方案用src/views/role/分层逻辑上完全可行。但你要接受一个事实随着功能增加路由守卫和权限跳转会越来越绕今天我遇到一个数据越权问题排查了半天就是因为骑手端的订单详情页引用了用户端的组件。拆工程之后这个坑自动消失了。4.2 token与路由守卫用前端先挡住不该进来的人每个端在登录成功后后端会返回JWT token和角色信息。前端把token存到localStorage里axios请求拦截器统一加上Authorization: Bearer xxx头。路由守卫是前端权限的第一道门。以商家端为例未登录用户访问任何页面都要重定向到登录页同时要校验当前用户的角色是商家否则就算拿到token也不能放行router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (!token) return next(/login) if (to.meta.role to.meta.role ! role) return next(/forbidden) next() })后端接口仍然要再做一次真正的权限校验前端守卫只解决体验问题不能只做前端判断。我在测试时发现一个典型问题商家端网页被复制给普通用户普通用户直接改localStorage里的role前端可能蒙混过关但后端的who_can_access中间件校验JWT里的用户类型后立刻返回403。这个流程才是安全闭环。4.3 订单状态刷新短轮询还是WebSocket我的取舍骑手端的场景是“不断刷新可抢订单”商家端的场景是“看到新订单提醒”用户端则是“订单状态实时更新”。一开始我想用WebSocket一劳永逸但实际写下来发现三个问题一是Django Channels的配置和部署复杂度明显提升二是后端要维护连接状态、处理重连逻辑三是Demo演示时一旦网络切换就断连效果反而不好。所以我最后采用了“短轮询页面离开清理”的方案。核心就两件事第一全局封装一个轮询hook每个页面组件自己决定轮询频率。比如骑手抢单列表5秒一次商家新订单提醒10秒一次用户订单详情15秒一次。频率不要统一减少不必要的请求。第二组件销毁时一定要清理定时器。这个坑我踩得很深一开始只设置了setInterval没有在beforeUnmount或onUnmounted里清理导致切换页面后后台还在不断请求接口造成严重的请求堆积。封装成可复用的usePolling函数后整个代码干净很多export function usePolling(fn, interval) { let timer null const start () { stop() timer setInterval(fn, interval) } const stop () { if (timer) clearInterval(timer) timer null } onBeforeUnmount(stop) return { start, stop } }WebSocket不是不能用如果你打算在这个项目上深入学习它确实是个加分项。但为了保质保量完成一个可展示的Demo轮询是投入产出比最高的选择。把轮询方案写清楚、组件销毁逻辑写对比一个偶尔掉线的WebSocket更经得起追问。5. PyCharm里的开发调试从创建项目到联调跑通的日常5.1 新建Django项目时别急着写代码先把环境配好用PyCharm新建Django项目时我的习惯是先在项目根目录创建虚拟环境然后在虚拟环境里安装依赖。这里有一个小坑PyCharm默认的Python解释器可能指向全局环境如果你有时候在自己的电脑上测没问题拿到别的电脑就报ModuleNotFoundError大概率就是解释器选错了。新建项目时选New environment using Virtualenv之后所有依赖都在这个虚拟环境里换机器复制依赖列表后一键恢复。依赖文件我一般分离三层requirements.txt requirements-dev.txt # 调试、测试工具 requirements-prod.txt # gunicorn、whitenoise等生产环境依赖本地开发用SQLite最省事但订单并发测试时SQLite会有限制建议在settings.py里通过环境变量控制数据库选择测试并发时切到MySQL。用PyCharm的Terminal启动后端后配合.env文件管理私密配置不要明文写在settings里。PyCharm专业版对.env文件有原生支持社区版也可以在启动配置里把环境变量填进去。5.2 调试模式的正确姿势用Debug按钮而不是Run按钮很多同学在PyCharm里直接点绿色三角的Run然后发现只输出日志不能断点。Django项目应该选择Debug模式运行在后端代码里打上断点前端Vue发起请求后请求会停在断点处此时可以查看所有局部变量、请求对象、数据库内容。这个体验远胜过print大法。如果同时要调试Vue前端建议在PyCharm的编辑器中打开Vue工程用Terminal运行npm run dev前端跑在8080端口后端跑在8000端口。然后通过vite或vue.config里的proxy配置把/api代理到后端避免跨域问题。前端到后端的跨域后端还要加django-cors-headers否则本地联调时浏览器会拦截所有非代理请求。我在实际项目里配置好proxy之后前后端完全同源cors配置反而变成了保险丝。5.3 两个容易被忽略的坑时区与Server Run配置第一个坑是时区。Django项目创建时默认TIME_ZONE UTC其实应该设置成自己的本地时区同时把USE_TZ True保留。如果你发现自己创建的订单时间比实际时间差8小时基本就是没改TIME_ZONE。第二个坑是Run Configuration里没选对DJANGO_SETTINGS_MODULE。当项目里有多个环境配置文件时调试时明明改了settings_dev.py启动后却还是读settings.py就是因为运行配置里的环境变量没指向对应文件。这个排查一度花了我半小时最后发现只是配置项选错了。还有一个小技巧在PyCharm的Debug窗口里可以把常用的接口请求做成一个HTTP Client文件例如.http文件。用Postman当然可以但PyCharm自带的HTTP Client可以直接复用debug会话里的环境变量测试token时不用手动复制。6. 把Demo变成能打动人的作品造数据、演示顺序与进阶扩展6.1 演示数据决定第一印象一个空荡荡的平台没有任何说服力。我强烈建议在项目里写一个批量造数据脚本用真实的图片、菜品名、商家名去填充数据库。脚本大概做这几件事创建3个商家、20个菜品每个菜品带分类和价格创建2个骑手、5个普通用户给每个商家生成营业时间和地址创建一两笔处于不同状态的订单方便演示时直接展示状态列表造数据脚本直接用Django的ORM写在PyCharm里用python manage.py shell或者单独跑脚本例如for i in range(20): Dish.objects.create( shopshop1, namef招牌菜品{i}, priceDecimal(f{10 i}.00), category热菜, imagedishes/demo.jpg, statusON_SALE )菜品图片是演示效果的关键建议直接从免费图库下载不要用纯色占位图。答辩或汇报时评委第一眼看到的是页面效果不是代码结构。如果你本地的Vue前端能看到色香味俱全的菜品页面这个项目的第一印象已经拿下了。6.2 演示完整闭环时我推荐的顺序整个演示最理想的状态是让观者看一遍完整的业务闭环商家登录上架菜品 → 用户登录下单 → 商家接单出餐 → 骑手抢单送达 → 用户确认完成。这套流程下来订单状态的变化会贯穿所有端观者对系统的理解立刻清晰。实际操作里我的建议是提前处理运费模板和商家营业状态。因为很多同学演示到下单这一步突然发现商家显示歇业或者运费计算报错整个后面的流程就断了。我在演示脚本里会让所有商家默认is_openTrue并且每种菜品库存设置得非常大这样演示时不会因为库存不足或打烊问题卡壳。另外演示时优先拿一个提前造好的“已完成”订单来展示订单列表和评价功能。完成状态的订单往往包含最长的时间线和最多的状态字段评委容易从列表里看到数据完整性。实时演示重点放在“下单”和“抢单”两个环节因为这两个部分交互最密集。6.3 值得继续深挖的四个扩展方向如果做完基础功能后还有余力我建议按下面的优先级扩展第一距离排序和骑手派单。给商家和用户地址都加上经纬度字段计算两点距离后按距离排序骑手端只展示距离自己较近的订单。这个功能未必多复杂但对外卖平台来说非常真实。第二订单超时自动取消。将Flask服务里写的超时扫描逻辑接入主系统比如支付后30分钟商家未接单则自动取消配送中超时可给用户提示。这个功能能体现你对业务细节的理解。第三优惠券和满减活动。在订单上增加优惠字段重新计算实付金额。扩展过程中自然会涉及一些收益计算和校验逻辑这部分的面试价值很高。第四消息推送。把第4章提到的WebSocket真正实现一遍让商家端和骑手端在订单状态变化时收到实时提醒虽然搭建成本高一些但做完之后整个项目的完成度会有质的提升。最后分享一个我实实在在踩过的坑千万别等全部代码写完再测试完整订单流程。我在做商家接单接口时因为太自信只测了接口返回成功没从头跑到尾结果等全部页面写完联调时发现商家接单后订单状态跳过了“待取餐”骑手端一直显示无单可抢排查了半天才发现是状态值写错了。最后我把状态迁移表打印出来贴在显示器边上每写一个接口都对照一遍后面基本再没犯过低级的状态错误。做这类多角色系统业务状态先理清代码只是把这些状态串起来而已。

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

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

免费获取报价 →
↑