news 2026/10/13 7:23:58

Django+Vue校园外卖平台全栈开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django+Vue校园外卖平台全栈开发实战

1. 校园外卖的需求画像:三方角色与核心业务流程

做校园外卖平台之前,我首先花了一周时间观察校园外卖的实际场景,而不是直接打开 IDE 写代码。校园外卖和美团、饿了么这类社会外卖最大的区别在于配送半径短、高峰期高度集中、用户群体相对封闭,而且很多商家本身就在校园内部或者校门旁边,这就决定了平台的功能设计不能照搬大厂模式。

我当时梳理出三个核心角色:学生用户、商家、配送员。可能很多人觉得还要加一个平台管理员,实际上校园外卖规模没那么大,管理端功能可以合并到系统后台,用 Django 自带的 Admin 就能覆盖。用户端要解决的核心痛点是:不知道今天吃什么、下单后不知道什么时候能送到、支付流程要尽量短。商家端的痛点是:订单消息容易漏看、菜品售罄了不想反复改、出餐后要快速通知配送员。配送端的痛点最实际:一次接多单的路线规划、送达后的状态反馈。

围绕这些痛点,我把核心业务流程画成了这样一条链路:用户浏览商家列表 -> 查看商家菜单 -> 加入购物车 -> 提交订单并支付 -> 商家接单 -> 商家出餐 -> 配送员取餐 -> 配送完成 -> 双方互相评价。这个流程看起来简单,但每一个环节都对应着明确的数据库表设计和 API 接口。举个例子,订单拆单问题就值得提前想清楚:用户在一个商家下单就是一张订单,如果用户想把 A 家的奶茶和 B 家的炒饭一起结算,那就涉及多商家拆单。我第一版选择不支持跨商家合并结算,因为校园外卖的客单价偏低,用户通常一次只在一家下单,强行做拆单会让订单状态管理复杂度翻倍。

功能清单我分为四个端来规划:

  • 用户端小程序/Web:微信登录或账号密码登录、商家列表(支持按距离、销量、品类排序)、菜单浏览、购物车、下单支付、订单列表与详情、订单状态跟踪、评价。
  • 商家端:菜品管理(上下架、价格库存)、营业状态开关、订单列表(待接单/制作中/已出餐)、接单与出餐操作、基础营业额统计。
  • 配送端:待取餐订单池、抢单/派单、取餐与送达状态更新、我的配送记录。
  • 管理后台:用户管理、商家审核与入驻、分类管理、订单概览、举报处理。

我实际开发时砍掉了配送端抢单逻辑的复杂度,改成管理员在后台手动指派配送员,或者配送员在配送端自己领单。校园配送员大多是兼职学生,抢单机制听着美好,实际会有人挂机抢单不配送,反而增加管理成本。第一版直接做成“订单完成后由系统广播给在线配送员,先到先得、超时自动释放”。

还有一个容易忽略的需求是“营业时间”。校园商家的营业时间非常不规律,有的只做午高峰、周末不营业,有的要提前一天预订早餐。如果不在商家端做营业时间配置,订单模块会不断收到用户的投诉电话。我在商家表里加了open_time、close_time、weekday_open几个字段,系统下单时先校验商家是否在营业中,这个逻辑很简单却非常实用。

2. 框架选型的真实考量:Django 和 Flask 之间,我为什么选 Django

标题里同时出现了 django 和 flask,这其实是很多人在项目起步时最纠结的问题。我最初也纠结过要不要用 Flask 保持轻量,但做完技术验证之后果断选了 Django,这里说说我的思考过程。

先看一组直接的对比:

对比维度DjangoFlask
内置功能Admin 后台、ORM、认证、表单、中间件齐全只有路由和模板核心,其余全凭扩展
学习曲线概念多,上手慢简单,改改就能跑
数据库操作ORM 自带 migrations 管理要自己接 SQLAlchemy,配置不少
适合场景业务逻辑复杂、实体关系多小型 API、微服务、原型验证
二次开发模型改完直接生成迁移文件要自己维护模型与表结构同步

校园外卖平台至少要包含用户、商家、菜品、订单、订单项、评价、配送记录这七张会互相外键关联的表,订单和订单项、商家和菜品、用户和评价之间全是嵌套关系。用 Flask 不是做不了,而是这些关系查询、联表筛选、后台 CRUD 都要自己造轮子。Django 的 ORM 在模型继承、关系查询、预取优化上非常顺手,比如查询订单时顺带取出用户、商家、订单项,用select_related和prefetch_related三行代码就搞定,同样的需求在 Flask 上要写大量 SQL。

Django 的 Admin 后台对这类管理型平台是巨大的生产力工具。商家审核、订单人工介入、菜品分类调整这些需求,直接用 Admin 就能实现,我几乎没有写任何后台页面前端代码。在项目第一版阶段,Admin 甚至可以直接充当商家管理端,后面才单独写 Vue 页面做商家自助操作。如果你用 Flask,这些后台页面全部得自己画,工作量直接翻倍。

我并不是完全否定 Flask。如果你的项目是纯 API 后端、客户端是独立的 App、没有后台管理需求,Flask 配合蓝图和扩展确实能写出结构优雅的代码。但校园外卖平台核心是数据密集型业务系统,用户认证、Session 管理、CSRF 防护、Admin 这些需求都是刚需,Django 全部内置并且久经考验,我不需要在安全配置上二次操心。选型从来不是比谁技术更酷,而是比谁能在同样时间内交付稳定可维护的东西。

3. 数据模型设计:订单状态机是整条业务链的心脏

我一直认为,外卖平台这类项目的数据库设计决定业务逻辑能走多远。表结构如果设计得不好,后面写 API 时会到处打补丁。我分享一下最终落地的核心模型设计,以及每个表背后的设计理由。

3.1 用户体系和角色划分

我没有单独建管理员表、商家表、配送员表,而是用 Django 的AbstractUser扩展出一个统一的用户模型,再通过role字段区分身份,同时维护一个独立 Profile 存扩展信息。这样做的原因是:一个用户在学校食堂窗口当商家,同时又可能兼职配送员,手机号登录同一账号就能切换身份。如果按角色拆成三张表,用户换身份时数据完全割裂,平台后续要做聚合统计就麻烦了。

扩展后的模型大概是这样的:

from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): ROLE_CHOICES = ( ('user', '普通用户'), ('merchant', '商家'), ('delivery', '配送员'), ('admin', '管理员'), ) phone = models.CharField(max_length=11, unique=True) role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user') avatar = models.URLField(blank=True) campus_address = models.CharField(max_length=200, blank=True) class Meta: db_table = 'auth_user_ext'

phone是登录凭证之一,验证码登录功能开发起来不难,但为了控制第一版复杂度,我先做了账号密码登录 + JWT,手机号作为唯一身份标识。这里有个关键点:Django 自带的 User 模型默认用username登录,你需要自定义认证后端把登录字段改成phone或者允许两者同时登录。

3.2 商家、菜品、分类的表关系

商家模型最容易被忽略的是地理位置字段。校园外卖虽然范围小,但配送距离判断、商家排序和配送费计算都需要经纬度。我在Merchant表里存了latitude和longitude字段,再用一个简单的 Haversine 公式在 Python 里计算距离,没有引入 PostGIS 和 GeoDjango,因为对校园这种几平方公里量级来说,直接用浮点计算完全够用了,省一套基础设施少掉一半部署问题。

菜品表的设计有一个老生常谈但实际很影响开发效率的点:分类和菜品的关系是单独建表,还是菜品表上挂分类字段?我建议在菜品表上直接挂category外键,但分类表隶属于商家,不同商家的分类互不相通。别搞全局分类,因为中餐、西餐、奶茶的分类体系完全不同,全局分类表会让前端菜单渲染非常难受。菜品表的status字段用来表示上架/下架状态,同时stock字段控制可售数量,售罄时前端直接展示“已售罄”而不是让用户下单后再被拒单。

3.3 订单表和订单状态机

订单表是整张表的索引命脉。我把订单核心字段设计为:

class Order(models.Model): STATUS_CHOICES = ( (0, '已取消'), (1, '待支付'), (2, '待接单'), (3, '制作中'), (4, '待配送'), (5, '配送中'), (6, '已完成'), ) order_no = models.CharField(max_length=32, unique=True) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders') merchant = models.ForeignKey(Merchant, on_delete=models.CASCADE, related_name='orders') delivery_user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name='delivery_orders') total_amount = models.DecimalField(max_digits=8, decimal_places=2) status = models.IntegerField(choices=STATUS_CHOICES, default=1) address = models.CharField(max_length=255) remark = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) paid_at = models.DateTimeField(null=True, blank=True) completed_at = models.DateTimeField(null=True, blank=True)

为什么要用IntegerField存状态值而不是直接用 CharField 字符串?因为代码里判断状态时用数字最简洁,if order.status == 3一眼就看明白,而且后续和前端联调时数字枚举映射关系清晰。Django 的choices参数只在表单和 Admin 展示层生效,不会影响数据库实际类型,这个设计很成熟。

整条链路最关键的是状态流转校验。我特意写了一个独立的服务函数集中处理状态变更,而不是在每个视图函数里直接order.status = 3然后save()。原因很简单:状态机一旦到处散落,后面加一个“退款中”状态时,你会找出几十处位置逐一修改,漏掉一处就产生脏数据。

class OrderMachine: TRANSITIONS = { 1: [2], # 待支付 -> 待接单 2: [3, 0], # 待接单 -> 制作中 / 取消 3: [4, 0], # 制作中 -> 待配送 / 取消 4: [5, 0], # 待配送 -> 配送中 / 取消 5: [6], # 配送中 -> 已完成 } def transition(self, order, target_status): if target_status not in self.TRANSITIONS.get(order.status, []): raise ValueError(f'订单状态不允许从 {order.status} 变更为 {target_status}') order.status = target_status order.save()

3.4 购物车与订单项的取舍

很多教程会把购物车做成数据库表,但我觉得第一版完全没必要。购物车是典型的临时性数据,用户关了浏览器就消失,做成 localStorage 存前端反而更简洁。你在前端维护一个购物车数组,提交订单时把商品 ID 和数量一起传给后端,后端在事务里创建订单和订单项。这样做省掉了购物车表的 CRUD API,而且前端交互更灵敏,增删商品不用等网络请求。

不过有一个细节必须处理干净:用户下单后到支付完成之间,菜品价格可能被商家调整。所以订单项里一定要保存“下单时的快照价格”,而不是下单时再去查菜品表的最新价格。

class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') dish = models.ForeignKey(Dish, on_delete=models.SET_NULL, null=True) dish_name = models.CharField(max_length=100) # 快照字段 dish_price = models.DecimalField(max_digits=6, decimal_places=2) # 快照价格 quantity = models.PositiveIntegerField(default=1)

为什么要冗余dish_name和dish_price快照字段?因为商家删掉菜品或者改价之后,你的历史订单列表依然能正确展示,否则就需要连表查一个已经不存在或者正在变化的菜品记录。这个设计在电商系统里叫“快照模式”,外卖平台必须用。

4. 后端接口实现:从登录鉴权到下单结算的完整链路

数据模型敲定后,后端就是沿着业务流把接口一个个写出来。我按模块分阶段实现,这里挑几个重点环节讲清楚实现思路和关键细节。

4.1 JWT 登录鉴权与自定义认证后端

Django 自带的 Session 认证在小程序端、前后端分离的场景下不是不能用,但每次请求都要带 Cookie,跨域设置麻烦,CSRF 处理也很啰嗦。我选了 Token 认证,使用rest_framework_simplejwt,它能自动处理 access token 和 refresh token 的签发与续期。

配置核心点是把登录标识从 username 改成手机号。我自定义了一个认证后端:

from django.contrib.auth.backends import ModelBackend from django.db.models import Q class PhoneBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): try: user = User.objects.get(Q(phone=username) | Q(username=username)) except User.DoesNotExist: return None if user.check_password(password) and self.user_can_authenticate(user): return user return None

设置文件里把AUTHENTICATION_BACKENDS指向这个类,同时在 simplejwt 的配置里指定USER_ID_FIELD和USER_ID_CLAIM。前端拿到 token 之后,在请求拦截器里放到Authorization: Bearer <token>头里,后端就能通过request.user拿到当前用户信息了。有一个坑值得提醒:simplejwt 的 refresh token 过期后用户要重新登录,开发阶段可以把SLIDING_TOKEN_REFRESH_EXPIRATION设长一点,减少调试时反复登录的烦躁感。

4.2 商家列表与菜品查询的索引和排序策略

商家列表接口的核心不是逻辑,而是性能和排序规则。校园外卖场景下用户会频繁按距离排序,如果每次请求都实时计算所有商家的经纬度距离再排序,数据量小还好,到了几千家商家时会卡。我的做法是写一个 Python 函数计算两点距离,然后对查询结果做内存排序,因为校园范围小、商家数量有限,内存排序完全够用。如果真要支撑城市级数据量,应该用 MySQL 的空间函数或者 PostGIS,但那样工程复杂度会上升。

菜品查询接口用了 Django ORM 的预取优化。家常写法是商家列表接口里嵌套循环查询菜品,产生 N+1 次数据库查询。我直接在序列化时用Prefetch把菜品一次性取出来:

merchants = Merchant.objects.filter(status=1).prefetch_related( Prefetch('dishes', queryset=Dish.objects.filter(status=1), to_attr='active_dishes') )

序列化器里只遍历merchant.active_dishes,一个商家一个列表。这个优化不复杂但效果立竿见影,接口响应时间从几百毫秒降到几十毫秒。

4.3 下单事务与库存扣减的原子性

下单接口是整个后端最容易出 bug 的地方。用户点一次“提交订单”,后端要做的事情包括:校验商家营业状态、校验菜品上下架和库存、计算订单总金额、创建订单记录、创建订单项、扣减菜品库存。任何一个步骤失败都不能让数据库出现半成品订单,所以必须用事务包住整段逻辑。

Django 的transaction.atomic()上下文管理器可以嵌套使用。我建议把创建订单的核心逻辑写成一个独立函数并用@transaction.atomic装饰器包裹,内部任何异常都会触发整体回滚。这里要注意一个和并发相关的坑:扣减库存时不能用“先查库存再判断够不够”的方式,因为高并发下两条请求同时查到库存为 1,然后都扣减成功,库存变成 -1。正确做法是用数据库的行级锁或者原子更新:

from django.db import transaction with transaction.atomic(): dish = Dish.objects.select_for_update().get(pk=dish_id) if dish.stock < quantity: raise ValueError('库存不足') dish.stock -= quantity dish.save()

select_for_update()会锁定这条菜品记录直到事务结束,第二个请求必须等待前一个事务提交后才继续执行,这样就不会出现超卖。

还有一个容易被忽略的问题:支付成功的回调。第一版我做了支付模拟接口,用户调用pay_order接口传入订单号,后端在校验订单状态为“待支付”后直接置为“待接单”,不接真实第三方支付。但支付回调一定是异步的,如果你后续接入微信支付或支付宝,需要在回调接口里做签名校验、金额校验、订单号校验,而且回调处理必须幂等,不能因为重试导致重复更新状态。这些我在第一版代码里就预留了接口位置。

4.4 状态变更接口与操作权限

订单状态变化涉及用户、商家、配送员三方不同的操作权限。用户能催单但不能把已支付的订单改成已完成,商家能接单但不能替用户确认收货,这些规则我在视图函数里通过request.user.role严格判断。

以商家接单为例:

@api_view(['POST']) def merchant_accept_order(request, order_id): order = get_object_or_404(Order, pk=order_id) if request.user.role != 'merchant': return Response({'detail': '无权操作'}, status=403) if order.merchant.user != request.user: return Response({'detail': '只能操作本店订单'}, status=403) machine = OrderMachine() try: machine.transition(order, 3) # 待接单 -> 制作中 except ValueError as e: return Response({'detail': str(e)}, status=400) # 这里触发通知逻辑 notify_user(order, '商家已接单') return Response(OrderSerializer(order).data)

平台后续如果要接入消息推送,这些notify_*函数就是扩展点,我把它们从视图逻辑里剥离出来,避免视图过度膨胀。

5. Vue 前端搭建:从页面拆分到接口联调

后端接口有了,前端我选了 Vue 3 + Vite + Pinia + Element Plus。重点讲一下页面结构怎么组织、Axios 怎么封装、以及前后端联调过程中最容易踩的坑。

5.1 前端页面结构与路由设计

我按角色维度拆分前端项目,而不是按页面维度堆在一起。项目目录大概是这样的:

src/ views/ user/ ShopList.vue ShopDetail.vue CartPage.vue OrderList.vue OrderDetail.vue merchant/ Dashboard.vue DishManage.vue OrderManage.vue delivery/ TaskList.vue login/ LoginPage.vue RegisterPage.vue router/index.js stores/user.js api/request.js

路由对象里用路由守卫控制权限,通过 Pinia 里存的用户角色判断当前路由是否允许访问。这个方案简单直接,但要注意一点:路由守卫只能做前端控制,真正的权限校验必须以后端接口返回的 403 为准,前端守卫只是用户体验层面的提前拦截。

5.2 Axios 封装和 Token 处理

Axios 封装是我每次项目都会做但很多教程不讲的部分。完整封装包含三块:请求拦截器统一加 token;响应拦截器统一处理业务错误码和 401 跳转登录;统一携带一些通用的请求头。

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000, }) request.interceptors.request.use((config) => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( (response) => response.data, (error) => { if (error.response) { if (error.response.status === 401) { localStorage.removeItem('access_token') router.push('/login') } else if (error.response.status === 400) { return Promise.reject(new Error(error.response.data.detail || '请求参数错误')) } } return Promise.reject(error) } )

这里有个细节我踩过坑:baseURL设置为/api后,开发环境怎么解决跨域?我推荐用 Vite 的代理而不是引入django-cors-headers。在vite.config.js里这样配置:

export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, }, }, }, })

这样前端页面里的请求都走http://localhost:5173/api,Vite 开发服务器把它代理转发到 Django 的 8000 端口。浏览器里看不到跨域,因为请求是同源的。生产环境则直接用 Nginx 反向代理同一套路径规则。相比之下,用django-cors-headers还需要维护允许的来源列表,开发时还经常因为ALLOWED_HOSTS配置漏掉地址而白屏,代理方案更省心。

5.3 购物车和页面状态管理

购物车我选择放在 Pinia 里存内存,同时镜像一份到 localStorage,刷新页面不丢失。用 Pinia 的好处是商家详情页加购之后,底部导航栏的购物车角标能响应式地更新。

export const useCartStore = defineStore('cart', { state: () => ({ items: JSON.parse(localStorage.getItem('cart_items') || '[]'), }), actions: { addToCart(dish, quantity = 1) { const existing = this.items.find((item) => item.dish.id === dish.id) if (existing) { existing.quantity += quantity } else { this.items.push({ dish, quantity }) } localStorage.setItem('cart_items', JSON.stringify(this.items)) }, clear() { this.items = [] localStorage.removeItem('cart_items') }, }, })

有个地方必须提醒:localStorage 里存的商品数据里包含价格,用户可能通过浏览器开发者工具手动修改 localStorage 里的价格再提交订单。所以前端购物车里的价格只能做展示,后端创建订单时必须以数据库里的菜品价格为准重新计算总金额。开发阶段我就发现过这个问题,提前做了防护。

6. PyCharm 下的双进程开发:环境配置与调试技巧

很多人把 PyCharm 当一个编辑器用,实际上它在 Django + Vue 这种双进程项目里能发挥很大价值。我在整个开发过程中总结了一套配置方案,能显著减少切换窗口和 Debug 的痛苦。

6.1 同时管理 Django 后端和 Vue 前端

PyCharm 专业版支持同时配置多个运行配置(Run Configuration)。我会建立两个配置:一个是 Django Server,启动命令是python manage.py runserver 8000;另一个是 npm 脚本配置,执行npm run dev启动 Vite 开发服务器。两个配置可以同时运行,PyCharm 右上角会并排显示两个服务。这样我改完后端代码在 PyCharm 里按 Ctrl+Shift+F10 重启,改完前端代码 Vite 会自动热更新,不用在终端窗口之间来回切换。

专业版对 Vue 单文件组件和高亮支持非常完整,而且在调试接口时可以直接用 PyCharm 内置的 HTTP Client(也就是.http文件)发送测试请求,不需要开 Postman:

### 用户登录 POST http://localhost:8000/api/token/ Content-Type: application/json { "username": "test_user", "password": "test_password" } ### 获取商家列表 GET http://localhost:8000/api/merchants/ Authorization: Bearer <token>

这种文件式接口调试的好处是请求历史、环境变量、断言逻辑都保留在项目里,换电脑也能直接接着用。

6.2 虚拟环境和依赖管理的坑

我建议用 PyCharm 创建项目时直接选择虚拟环境,并把 Django 和 DRF(Django REST Framework)装进去。这里有一个非常容易踩的坑:默认的python解释器可能是系统全局环境,装了一堆乱七八糟的包,结果某个依赖版本冲突导致 Django 启动崩溃。用虚拟环境能隔离项目依赖,专案专用。

依赖清单里我认为必须装的是这些:

  • Django(我用的是 4.2 LTS 版本)
  • djangorestframework
  • djangorestframework-simplejwt
  • django-cors-headers(虽然 Vite 代理规避了跨域,但生产环境还是可能需要)
  • mysqlclient 或 pymysql(看数据库选型)
  • pillow(图片上传处理,商家上传菜品图必需)

有一个必须注意的坑:Django 3.2 以上对auto_now_add和auto_now字段的处理没有变化,但是 MySQL 8 默认字符集如果是 utf8mb4,迁移时可能会因为索引长度问题报错。我遇到过一次Specified key was too long的错误,解决方案是在数据库连接配置里显式指定字符集为 utf8mb4,或者在 Django 的settings.py里加上OPTIONS = {'charset': 'utf8mb4'}。

6.3 断点调试的实战心得

Django 项目在 PyCharm 里打断点调试非常方便,但很多人不知道调试代码时能不能看到 SQL 语句。我通常会在断点处切到 Console 窗口,然后手动执行:

from django.db import connection print(connection.queries)

这样就能看到当前请求执行的全部 SQL,排查 N+1 查询问题直接一目了然。配合django-debug-toolbar可以在网页底部看到直观的 SQL 统计,但我个人觉得在开发量不大的时候,connection.queries一梭子就能解决问题,装上调试工具栏反而拖慢页面响应。

另一个实用技巧是调试异步请求。Vue 前端发起的 Axios 请求进入 Django 视图函数后,PyCharm 的调试器可能停在断点处,但 Vite 那边还在等待响应,这是正常的。你在 PyCharm 里按 F8 单步执行,前端页面会在请求返回后自动更新,体验基本接近全栈调试器。

7. 部署与优化:上线前必须处理的几个关键问题

项目开发完不是终点,能稳定跑起来交付给同学用才叫完成。这里讲讲部署方案和上线前我做的几项关键优化。

7.1 轻量部署方案:Nginx + Gunicorn + Django

校园项目不需要上 Docker 全家桶,一个轻量云服务器加 Nginx 就能扛住日常请求。我的部署方案是:Gunicorn 充当 Python WSGI 服务器运行 Django 应用,监听本机 8000 端口;Nginx 监听 80 端口,把/api路径的请求转发到 Gunicorn,同时托管前端构建后的静态文件。

前端构建后产物在dist/目录里,把里面的文件放到 Nginx 的 web 根目录或者单独指定目录就行。Nginx 核心配置大概长这样:

server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/html/campus-takeout/dist; try_files $uri $uri/ /index.html; } }

有一个部署的细节必须牢记:Django 的DEBUG模式在线上一定要设成 False,否则一个未捕获异常会直接把栈信息完整吐给用户,安全风险巨大。同时要配置ALLOWED_HOSTS为你的服务器域名或 IP。收集静态文件用python manage.py collectstatic,再把收集到的静态文件交给 Nginx 托管,否则 CSS 和图片会全部 404。

7.2 数据库层面的索引与查询优化

上线前我检查了每个高频接口的数据库查询,发现订单列表接口存在明显的性能问题:按用户查询订单时,时间排序走的是created_at字段,但这个字段没有索引,数据量上去后会全表扫描。我增加了数据库索引:

class Order(models.Model): ... created_at = models.DateTimeField(auto_now_add=True, db_index=True)

同时给订单的status字段加索引,因为运营后台要高频按状态筛选订单。另外一个常见的优化是分页,Django REST Framework 内置的PageNumberPagination直接可用,我配置每页 20 条,前端滚动加载时按页请求。校园学生用户量大,订单表几百条数据就开始卡的话,体验会很差。

还有一类优化是不起眼但极其实用的:菜品图片不要直接存在本地磁盘里然后让 Django 去读文件。开发时可以先服务本地媒体文件,但上线后建议把图片放到对象存储里,数据库只存 URL 字段。对象存储的成本非常低,但能显著降低 Web 服务器负载,并且图片加载速度快很多。

7.3 上线后的几个真实教训

项目上线一周后,我遇到的真实问题和我预期的技术难点完全不一样。

最大的教训是密码安全策略不要图省事。我在 User 模型里给密码设了简单的正则校验,结果有同学注册了 8 位纯数字密码,被撞库攻击后在别家平台被盗号,回头投诉到我这。后来我统一把密码校验策略调高,至少包含字母和数字,并且把登录接口加了简单的限流,每个手机号一分钟最多尝试五次。

第二件事是订单并发下的状态混乱。开发时我注意力全放在库存扣减的原子性上,忘了订单状态本身在高并发下也可能被重复更新。用户在支付页秒点两次“提交订单”,后端可能创建出两条订单。我加了前端防重复提交,同时在订单号生成上使用 UUID + 时间戳保证唯一,并且在后端对相同用户在短时间内的订单创建做了幂等控制。

第三件事是商家端权限要管到菜单链接级别。我最初只在前端路由做了角色限制,但后来发现有人通过修改前端 JavaScript 或者直接访问接口路径,能拿到商家端的 API 请求。真正的安全防线永远在服务端,前端路由守卫只是体验层的优化。我把商家端的每个接口都加上了角色和所有权校验,宁可多写几行判断也不能让数据裸奔。

写在最后的一点心得

校园外卖项目我前后花了大约四周时间,从需求梳理到部署上线,最大的感受是:这类全栈项目真正的难点不是某个单独技术,而是把各个模块的逻辑边界梳理清楚。Django 负责把数据模型和业务规则稳住,Vue 负责让用户交互更流畅,PyCharm 负责把两种语言的开发体验整合到一条工作流里,三者各司其职,项目就不会失控。

如果再给我一次机会重新开始,我会在第一天就把订单状态机的测试写完整。当时我嫌单元测试麻烦,觉得函数逻辑简单没必要测,结果后来改动模型字段时,一个不起眼的状态变更遗漏导致线上订单卡在“制作中”整整两小时。这类业务系统里,自动化测试从第一天开始写并不亏,后面每次重构都有安全网兜底。如果你想做类似项目,建议把这条经验也记下来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 7:23:03

Canon EDSDK 相机二次开发实战:从设备枚举到自动化采集全攻略

简介&#xff1a;佳能EDSDK 3.6.1&#xff08;EOS Digital Solution Development Kit&#xff09;是一套面向C#、VB和C开发者的相机控制开发包&#xff0c;用于在Windows桌面应用中实现与佳能数码相机的通信与操控&#xff0c;可完成实时预览、拍摄录像、曝光参数调整、设备管理…

作者头像 李华
网站建设 2026/10/13 7:22:53

GitHub贡献图刷绿原理与autoCommit实战指南

简介&#xff1a;这是一款基于 TypeScript 开发的 VSCode 插件&#xff0c;面向希望美化 GitHub 贡献图、灵活管理提交记录的前端开发者与开源爱好者。它支持一键批量提交 commit&#xff0c;可自由指定过去几年乃至未来日期的提交&#xff0c;既能固定次数也能随机次数&#x…

作者头像 李华
网站建设 2026/10/13 7:22:31

SpringBoot整合Kafka实战:从基础配置到生产级架构

SpringBoot整合Kafka&#xff1a;构建高效消息驱动应用做后端开发的&#xff0c;这几年基本都绕不开Kafka。我自己从最早在项目里引入Kafka做日志异步采集&#xff0c;到后来负责电商订单状态同步、用户行为轨迹管道&#xff0c;再到帮团队搭建基于Kafka的Event-Driven架构&…

作者头像 李华
网站建设 2026/10/13 7:22:27

从CIDR到路由聚合:网络边界、前缀计算与排错实战

我最早对 CIDR 产生敬畏心&#xff0c;是在一次给分公司做路由汇总的深夜。公司刚接入了一个新办公区&#xff0c;路由表里本来静静躺着几条 /24 的明细路由。我贪省事&#xff0c;想着合并成大网段能少几行配置&#xff0c;就在出口设备上写了一条更大的汇总路由。结果第二天工…

作者头像 李华
网站建设 2026/10/13 7:22:21

高校学生社团管理系统毕设实战:SpringBoot+MyBatis+MySQL全解析

关注可免费领取源码——这个标题在各类博客、GitHub、甚至是毕设资料群里都太常见了。如果你正好选到了“高校学生社团管理系统”这个题目&#xff0c;或者在选题库里徘徊不知道选什么&#xff0c;那我先说一句&#xff1a;这个题目属于典型的“管理信息系统”类毕设&#xff0…

作者头像 李华
网站建设 2026/10/13 7:22:18

用户态TCP/IP协议栈实战:从tcpip_stack_v1_2.zip到可控网络实验

简介&#xff1a;这是一份基于Verilog实现的完整TCP/IP协议栈工程源码&#xff0c;面向FPGA网络通信开发者与高校嵌入式方向学习者&#xff0c;可在Xilinx Spartan6平台上完成从MAC到TCP的全链路收发验证。工程在ISE14.7环境下开发&#xff0c;配合黑金AX516开发板与PHY接口芯片…

作者头像 李华