☰
小程序+Django在线点餐毕设:从架构到避坑的完整实战
2026/10/6 10:18:22 网站建设 项目流程

简介:面向本科毕业设计场景,这份在线点餐系统资源提供了微信小程序前端与Python-Django后端完整实现,适合需要完成前后端协同项目的计算机专业学生参考。包内共2000个文件,其中1474个Python文件构成后端核心逻辑,201个HTML模板与30个CSS用于后台管理页面,127个JS及13个WXSS、11个WXML支撑小程序交互与样式,另有JSON配置文件、pyc编译文件等,整体18.39MB,目录结构清晰。系统覆盖用户端点餐、购物车、订单管理和商家端菜品维护、数据统计等模块,并包含微信支付集成相关代码。已有98人学习下载,适合作为毕业设计选题论证、系统设计或代码复现的直接素材,能帮助理解前后端分离架构与RESTful API实际落地。

1. 小程序 + Django 在线点餐毕设:它解决的不只是“能跑”

拿到的这份毕设压缩包,是典型的“小程序前端 + Django 后端”组合:前端是原生微信小程序,点餐页面、购物车、订单列表都在微信开发者工具里跑;后端是 Python 的 Django,配合 DRF(Django REST Framework)对外提供 JSON 接口。不少同学对着这种工程第一反应是“先跑起来”,结果卡住的往往不是环境,而是订单状态怎么流转、token 怎么带、图片地址为什么裂了这三件事。这份资源适合三类人:毕设、课设时间紧,需要一周内复现出完整演示效果的同学;第一次写前后端分离项目、想在真实源码里看接口与页面怎么对上的人;以及需要用小程序端做答辩演示、但又不想从零写一套点餐系统的毕业生。下面按我拆解这类项目的顺序,把架构、后端、小程序端和最容易翻车的细节过一次。

2. 架构与数据模型:为什么选 Django DRF,五张表怎么划分

2.1 为什么毕设要用小程序原生 + Django + DRF

微信小程序不是传统网页,它是一个“宿主固定”的前端容器,所有业务数据都得从后端接口拿。原生小程序用 WXML、WXSS、JS 写页面,语法和 Vue 有相似处,但调试和发布都走微信开发者工具,这一套对毕设来说是现成的前端交付物。后端选 Django,最大的理由是快:自带 ORM、Admin 后台、用户认证体系,写一个带菜品管理和订单管理的后台,几乎不用重复造轮子。再叠上 DRF,模型可以直接序列化成 JSON 给小程序消费,接口的增删改查也只需要几行代码。

这类项目常见的另一种组合是“Spring Boot + 小程序”,但 Python 栈在毕设里上手成本更低,遇到问题搜到的资料也更多——尤其是“django 项目实战新手”这个方向,几乎每个坑都有人踩过。我一般会保留 Django Admin 后台,把它当成临时菜单录入工具:菜品、价格、库存、分类都直接在 Admin 里改,小程序端只读接口数据。这样做的好处是前端不用做一个完整的管理页面,答辩时展示数据维护也方便。前后端分离的边界其实很清晰:小程序负责渲染与交互,Django 只管数据和校验,两边通过 JSON 交换,互不侵入。

还需要明确一点:前端“微信小程序”关键词对应的不是 HTML 页面,而是小程序里的页面栈。点餐主页、菜单分类、购物车、订单确认、我的订单这些页面,在小程序里都对应独立的 Page。每个 Page 通过 wx.request 请求后端,拿到 JSON 后 setData 渲染到 WXML 上。这个结构决定了后端接口必须按页面维度来设计,而不是按模块维度堆大接口。

2.2 数据模型:从分类到订单明细的 5 张表

点餐系统最核心的数据模型其实不多,正常毕设规模五张表就够:分类表、菜品表、购物车表、订单表、订单明细表。我拆这类工程时习惯先看 models.py 再跑代码,因为表结构决定了接口长什么样,也决定了前端页面能不能对上字段。

表名关键字段用途
Categoryname, sort, status菜品分类,如“热菜”“凉菜”“饮品”
Dishname, category, price, image, stock, status菜品信息,外键指向分类
CartItemuser, dish, quantity购物车条目,一个用户对应多条
Orderuser, order_no, address, total_amount, status, remark订单主表
OrderItemorder, dish_name, price, quantity订单明细,冗余菜品名和价格

下面是模型的核心代码片段,直接对应 Dish 和 OrderItem 两张表:

# dish/models.py from django.db import models class Category(models.Model): name = models.CharField(max_length=50, verbose_name='分类名') sort = models.IntegerField(default=0, verbose_name='排序号') status = models.BooleanField(default=True, verbose_name='是否上架') class Meta: ordering = ['sort', 'id'] class Dish(models.Model): category = models.ForeignKey( Category, on_delete=models.PROTECT, related_name='dishes', verbose_name='所属分类' ) name = models.CharField(max_length=100, verbose_name='菜名') price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='单价') image = models.CharField(max_length=255, blank=True, verbose_name='图片URL') stock = models.IntegerField(default=0, verbose_name='库存') status = models.BooleanField(default=True, verbose_name='是否上架') class Meta: ordering = ['-id']

这里有两个参数要特别说明。on_delete=models.PROTECT的作用是:如果这个分类下还有菜品,坚决不允许删除分类,防止小程序端出现“菜品分类为 null”的悬空数据。很多初学项目会写成CASCADE,结果删掉一个分类,连带把菜品也删了,这在小程序端就是一场小型翻车事故。DecimalField(max_digits=8, decimal_places=2)是给价格用的,不要用 FloatField 存金额,浮点数累计到一定量会产生精度误差。OrderItem表要故意冗余dish_name和price两个字段,因为订单一旦生成,菜品可能改名、改价,订单明细应该保留“下单那一刻”的快照。

2.3 环境与配置:SQLite 快速开发、MySQL 放部署

毕设的合理路线是:本地先用 SQLite 把功能跑通,最后再决定要不要切 MySQL。SQLite 零配置,Django 默认就支持,数据库文件就是一个 db.sqlite3,放在工程目录里,整个项目拷走就能复现。MySQL 适合部署到云服务器上,但 Windows 下安装 mysqlclient 驱动偶尔会编译报错,一开始就卡在驱动上不值得。

关键配置在 settings.py 里,默认数据库配置可以直接跑:

# online_order/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'rest_framework_simplejwt', 'corsheaders', 'dish', 'order', ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), }

INSTALLED_APPS里的顺序有讲究:先加载 Django 自带模块,再加载 rest_framework,最后才加载自定义 app。JWT认证配置放在REST_FRAMEWORK里是全局生效的,后面小程序端请求带上Authorization: Bearer <token>就能识别用户。数据库切 MySQL 时只需要把ENGINE改成django.db.backends.mysql,再补上NAME、USER、PASSWORD、HOST、PORT就好。表格对比一下两种数据库在毕设里的取舍,SQLite 胜在零配置,MySQL 胜在答辩时更贴近生产环境;我的习惯是先 SQLite 后切换,不要在项目起步阶段两件事一起干。

3. 后端接口的搭建顺序:分类、菜品、下单三个核心接口全跑通

3.1 初始化项目与 app:Django 管理命令三件套

拿到源码后先不要急着启动,按下面这个顺序把项目结构过一遍。如果这份压缩包里的代码是完整的,那它通常已经包含一个 Django 工程和一个或多个 app;如果是从零复制重建,流程是一样的。

# 创建工程,online_order 是工程名 django-admin startproject online_order . # 创建两个 app:dish 管菜品,order 管订单 python manage.py startapp dish python manage.py startapp order # 注册 app 后生成并应用迁移 python manage.py makemigrations python manage.py migrate # 创建管理员账号,用于登录 Admin 后台 python manage.py createsuperuser

startproject生成的是工程级目录,包含 settings.py、urls.py 这些全局配置;startapp生成的是业务模块,里面是 models.py、views.py、admin.py。很多新手容易把业务代码全塞进一个 app,我一般会按业务边界拆开:点餐的菜品/分类放dish,订单和购物车放order,这样后面加功能时不会在同一个 models.py 里越改越长。makemigrations是生成迁移文件,migrate才是真正把表建进数据库,两者经常被连着执行,但本质上一个是“生成 SQL 变更计划”,一个是“执行变更”。执行完这些命令,Django 自带的 session、auth 等表就会出现在数据库里,Admin 后台也就有地方存用户和认证数据了。

3.2 序列化器与视图:DRF 里最省事的接口写法

后端接口的核心工作就是把模型转成 JSON,再接收小程序传过来的 JSON 转回模型。DRF 的ModelSerializer把这件事变成了声明式写法。先看序列化器:

# dish/serializers.py from rest_framework import serializers from .models import Category, Dish class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = ['id', 'name', 'sort'] class DishSerializer(serializers.ModelSerializer): # source 表示从外键关联对象的 name 字段取值 category_name = serializers.CharField(source='category.name', read_only=True) class Meta: model = Dish fields = [ 'id', 'name', 'category', 'category_name', 'price', 'image', 'stock', 'status' ]

category_name是查询菜品列表时最常用的字段:小程序首页要显示“宫保鸡丁 - 热菜”,这里的“热菜”就是从前端的角度直接把分类名拼进列表数据里,省得小程序端再做二次关联查询。source参数指明数据来源是category.name,因为Dish.category本身是外键对象,序列化时默认只输出 id,需要手动把关联字段拉平。接下来是视图层,用@api_view写一个带分页的菜品列表接口:

# dish/views.py from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.pagination import PageNumberPagination from .models import Dish from .serializers import DishSerializer @api_view(['GET']) @permission_classes([AllowAny]) def dish_list(request): # 只返回上架状态的菜品 dishes = Dish.objects.filter(status=True) category_id = request.GET.get('category_id') if category_id: dishes = dishes.filter(category_id=category_id) paginator = PageNumberPagination() paginator.page_size = int(request.GET.get('page_size', 10)) page = paginator.paginate_queryset(dishes, request) serializer = DishSerializer(page, many=True) return paginator.get_paginated_response(serializer.data)

这个接口对应了微信小程序里“页面列表加载更多”的完整链路。前端传page=1&page_size=10,后端返回一个包含count、next、results三个字段的分页对象;小程序端拿到results渲染当前页,用count判断还有没有下一页。permission_classes([AllowAny])表示菜品列表不需要登录就能看,这是点餐场景的合理设计——用户没登录时可以先浏览菜单,下单时才要求登录。后面下单接口就不能用AllowAny了,得换成IsAuthenticated。

3.3 下单逻辑:状态流转、库存校验、订单号生成

下单是整个后端最核心的业务逻辑,也是毕设答辩时最容易被打穿的地方。一个合格的订单接口要做四件事:从购物车取数据、校验库存、扣减库存、生成订单和明细。下面这段代码是标准写法,用事务把整个过程包起来,防止中途出错留下脏数据:

# order/services.py from decimal import Decimal from django.db import transaction from django.db.models import F from dish.models import Dish from .models import CartItem, Order, OrderItem @transaction.atomic def create_order(user, address, remark=''): cart_items = CartItem.objects.filter(user=user) if not cart_items.exists(): raise ValueError('购物车为空') total = Decimal('0.00') order = Order.objects.create( user=user, address=address, remark=remark, status=1, # 1: 待支付 order_no=generate_order_no() ) for item in cart_items: # select_for_update 锁住菜品行,避免并发下单超卖 dish = Dish.objects.select_for_update().get(id=item.dish_id) if dish.stock < item.quantity: raise ValueError(f'{dish.name} 库存不足') OrderItem.objects.create( order=order, dish_name=dish.name, price=dish.price, quantity=item.quantity ) # F() 表达式实现原子扣减 dish.stock = F('stock') - item.quantity dish.save(update_fields=['stock']) total += dish.price * item.quantity # 重新按订单 id 更新总价 Order.objects.filter(id=order.id).update(total_amount=total) cart_items.delete() return order

select_for_update()是数据库行锁,两个用户同时下同一道菜时,后一个请求会等前一个请求提交事务后才读取库存。F('stock') - item.quantity是直接把扣减操作下推给数据库执行,避免先SELECT再UPDATE造成的重复扣减。订单状态用数字表示,1待支付,2已支付,3制作中,4已完成,5已取消,状态流转在小程序端通过按钮调接口推动,后端只校验“当前状态下能不能跳转到目标状态”。order_no生成用当前时间戳加随机四位数字,命中的概率几乎可以忽略。这段业务逻辑看着不长,但事务、锁、金额精度三个点都覆盖了,答辩老师问起来也能讲明白设计意图。

4. 小程序端的对接方式:请求封装、JWT 登录态与触底加载

4.1 wx.request 统一封装与后端域名配置

小程序端最忌讳每个页面都裸写wx.request,一旦 token 过期要改逻辑,得把所有页面翻一遍。我一般会在utils/下做一个统一封装,把 baseUrl、请求头、错误处理全部收敛到一个文件里:

// utils/request.js const config = require('./config') function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: config.baseUrl + path, method, data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (wx.getStorageSync('token') || '') }, success(res) { if (res.statusCode === 401) { // token 失效,清理本地登录态 wx.removeStorageSync('token') wx.navigateTo({ url: '/pages/login/login' }) reject(new Error('未登录')) } else if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data) } else { reject(new Error(res.data.message || '请求失败')) } }, fail(err) { reject(err) } }) }) } module.exports = { request }

封装成 Promise 之后,页面里可以用async/await写异步逻辑,不用再套一层success回调。Authorization头统一在这里拼好,后续每个页面调用时不用关心 token 怎么传。配置文件单独放:

// utils/config.js module.exports = { baseUrl: 'http://127.0.0.1:8000/api' }

本地调试时,小程序模拟器可以直接访问电脑上的127.0.0.1;但真机预览时手机访问不了电脑的回环地址,必须把 baseUrl 改成电脑在局域网里的 IP,比如http://192.168.1.101:8000/api。上线时则要换成 HTTPS 域名,并且这个域名必须在微信公众平台的小程序后台配置成合法 request 域名。这三层环境切换是最常见的配置坑,我会直接在 config.js 旁边写注释区分“模拟器 / 真机 / 线上”三种情况。

4.2 登录与 token:wx.login 换成后端 JWT 的完整链路

在线点餐小程序不需要复杂的注册流程,最常见的方案是微信静默登录:前端调用wx.login()拿一个临时 code,传给后端;后端用这个 code 向微信接口换取 openid,再为这个 openid 生成 JWT 返回给前端。具体到毕设项目,如果没有接真实微信登录,也可以在后端提供一个传userId的调试接口,但接口结构要保持一致,前端代码不需要改。

// pages/login/login.js const { request } = require('../../utils/request') Page({ async onLogin() { try { // wx.login 获取临时凭证 code const { code } = await wx.login() const res = await request('/auth/login/', 'POST', { code }) // 后端返回 { access: '...', user_info: {...} } wx.setStorageSync('token', res.access) wx.setStorageSync('userInfo', res.user_info) wx.switchTab({ url: '/pages/index/index' }) } catch (e) { wx.showToast({ title: '登录失败', icon: 'none' }) } } })

这里的核心思路是“小程序不直接暴露 openid,而是让后端去微信那边换”。登录成功后,access是 JWT token,小程序存进本地 Storage,后续所有请求都由 4.1 里的请求封装自动带上。JWT 的过期时间默认是 5 分钟,实际开发时可以适当调长;对于毕设项目,我通常的做法是让后端把过期时间设置为 24 小时,同时每次打开小程序时重新静默登录刷新 token,这样既不用实现 refresh token 的完整流程,也能保证答辩演示时不会突然 401。

4.3 菜品列表触底加载与购物车联动

小程序列表页的“加载更多”靠onReachBottom生命周期触发,这是原生小程序专门为滚动到底部设计的事件。配合后端的分页接口,写法如下:

// pages/dish/list.js const { request } = require('../../utils/request') Page({ data: { list: [], page: 1, pageSize: 10, hasMore: true, categoryId: 0, cartCount: 0 }, async loadDishes(reset = false) { if (!reset && !this.data.hasMore) return const page = reset ? 1 : this.data.page const { results, count } = await request('/dishes/', 'GET', { page, page_size: this.data.pageSize, category_id: this.data.categoryId }) this.setData({ list: reset ? results : this.data.list.concat(results), page: page + 1, // 已加载数量小于总数时,允许继续触底加载 hasMore: this.data.list.length + results.length < count }) }, onReachBottom() { this.loadDishes() }, onPullDownRefresh() { this.loadDishes(true) wx.stopPullDownRefresh() } })

hasMore的计算逻辑最容易写错:应该用“当前已加载数量 + 本次返回数量”是否小于count来判断。如果直接写成results.length < count,那第二页加载完就会错误地关闭加载。concat是把新旧数组合并,而不是覆盖;reset=true时清空列表重新加载第一页,对应切换分类和下拉刷新的场景。

购物车联动在小程序端用wx.setStorageSync做本地缓存,加购时同步更新 tabBar 角标和购物车的总价。注意购物车数据的双写机制:本地缓存为了页面即时反馈,后端购物车接口为了下单时能拿到准确数据。这两个数据源要保持一致,否则会出现“页面显示有货,下单却提示库存不足”。下一章会讲这个问题的具体场景。

5. 避坑手册:毕设里最容易翻车的五个细节

5.1 菜品图片全部“裂了”

现象:小程序首页能显示菜品名称和价格,但图片位置一片空白或者显示破碎图标。

原因:Django 默认把图片当作静态资源处理,但 settings.py 里没有配置MEDIA_ROOT和MEDIA_URL,或者小程序端拼接图片 URL 时缺少域名前缀。另一个常见原因是菜品的image字段存的是相对路径,比如/media/dishes/c1.jpg,小程序端直接用这个路径发起图片请求,结果请求打到了http://127.0.0.1:8000/media/...以外的奇怪地址上。

解决:在 settings.py 里加两行配置:

MEDIA_ROOT = BASE_DIR / 'media' MEDIA_URL = '/media/'

然后在 urls.py 里补上静态资源访问路由,再保证前端image字段拼上config.baseUrl去掉/api后的域名前缀。小程序端<image>组件的src必须是完整 URL,不能是相对路径。我一般会直接在后端序列化器里把image拼成完整地址输出,这样小程序端拿到什么就渲染什么,不用每个页面再做拼接。

5.2 删除分类后,菜品列表出现空分类

现象:在 Admin 后台删掉某个分类,小程序端菜品列表里这一类的菜还在展示,但分类名变成了空白,或者点击分类标签时报错。

原因:外键on_delete被设成了默认行为,删除分类时 Django 会把菜品的category_id置空,但菜品本身没被删除。更隐蔽的情况是用了CASCADE,分类删了,菜品也跟着被删,用户看到菜品数量骤减。

解决:外键定义必须明确on_delete策略。菜品属于强依赖分类的业务,删分类时应该拒绝删除,或者先人工处理该分类下的菜品。代码上把外键改成on_delete=models.PROTECT,这样 Django 会在删除分类时抛出ProtectedError,从根上杜绝悬空数据。如果确实要删分类,就按“先转移、再删除”的顺序操作:先用 Django ORM 的查询把菜品的分类改到其他分类下,再删除原分类。对应的查询删除对象操作类似Dish.objects.filter(category_id=old_id).update(category=new_category)。

5.3 订单时间比本地时间早了 8 小时

现象:用户下单后,订单详情页显示的时间是凌晨,而实际下单时间是下午。

原因:Django 默认USE_TZ=True,会把时间按 UTC 时区存储和输出。如果 settings.py 里没有显式配置TIME_ZONE,默认值是UTC,而中国在东八区,差 8 小时。

解决:在 settings.py 里把时区显式指到北京时间:

TIME_ZONE = 'Asia/Shanghai' USE_TZ = True

要注意USE_TZ=True时,Django 存储到数据库里的时间仍然是 UTC,只是在输出到模板或序列化器时按TIME_ZONE转换。所以后端接口返回的时间字段在小程序端看起来会正常;但如果你自己写 SQL 查数据库,看到的数据还是 UTC,这不算 bug。小程序端展示时间时,建议直接使用后端返回的 ISO 字符串,再用new Date(...)格式化,不要手工拼时间字段。

5.4 微信开发者工具能跑,一上线就“网络不给力”

现象:本地模拟器里所有接口正常,但用预览二维码在真机上打开,页面一直报“请求失败”或“网络不给力”。

原因:微信小程序正式环境的网络请求有严格限制,request 的域名必须是 HTTPS,且需要在小程序管理后台配置为合法域名。开发者工具里勾选的“不校验合法域名”只对本地调试生效,真机预览和线上版本完全不认这个配置。本地调试时后端跑在http://127.0.0.1:8000是 http 协议,真机根本不会放行。

解决:把“不校验合法域名”当作开发期的临时工具,不要依赖它。上线前要准备一台云服务器、一个备案过的域名和 HTTPS 证书,把 Django 部署到服务器上,域名配置成https://开头,然后在微信公众平台“开发设置”里把这个域名加入 request 合法域名列表。毕设答辩如果没有条件买服务器,至少也要在真机预览时把 baseUrl 改成电脑的局域网 IP,并让电脑和手机处于同一个 WiFi 下,保证演示过程不翻车。

5.5 两个用户同时下单,库存变成负数

现象:某道菜库存只剩 1 份,两个用户几乎同时下单,结果两个订单都创建成功,库存变成 -1。

原因:下单逻辑先查询库存再扣减库存,这两步之间有时间窗口。两个并发请求同时通过了“库存充足”的判断,随后各自扣减,库存就被扣穿了。

解决:在扣减库存时使用数据库层面的原子操作,把“判断库存 + 扣减库存”合并成一条 SQL。Django ORM 里最直接的做法是:

from django.db.models import F # 只有当库存大于等于购买数量时才执行扣减 updated = Dish.objects.filter( id=dish_id, stock__gte=quantity ).update(stock=F('stock') - quantity) if updated == 0: raise ValueError('库存不足')

F('stock') - quantity会在数据库内部完成“读取旧值、减数量、写回新值”三个动作,不经过 Django 层的缓存。配合前面 3.3 小节里的select_for_update(),能挡住绝大多数超卖场景。注意这两个方案针对的是不同层面的问题:select_for_update()锁住整行直到事务提交,适合订单创建时需要读菜品价格等完整信息的场景;F()表达式则适合单纯扣数字的秒杀场景。毕设项目用其中一种就足够,两种都写也不冲突。

6. 验收与进阶:写一段冒烟脚本,五分钟跑完全部接口

6.1 不打开小程序也能完整跑通业务链路

答辩前最怕的事是打开小程序演示时才发现后端某个接口挂了。我现在的习惯是准备一段 Python 冒烟脚本,直接用requests模拟小程序的请求链路,从登录、查菜单、加购物车、下单到查订单,把核心接口全走一遍。脚本长这样:

# smoke_test.py import requests BASE = 'http://127.0.0.1:8000/api' # 1. 登录获取 token login_res = requests.post(f'{BASE}/auth/login/', json={'code': 'test_code'}) token = login_res.json()['access'] headers = {'Authorization': f'Bearer {token}'} # 2. 查分类和菜品列表 categories = requests.get(f'{BASE}/categories/').json() dishes = requests.get( f'{BASE}/dishes/', params={'page': 1, 'page_size': 5} ).json() first_dish = dishes['results'][0] print('第一个菜品:', first_dish['name'], first_dish['price']) # 3. 加购物车并下单 requests.post( f'{BASE}/cart/', headers=headers, json={'dish_id': first_dish['id'], 'quantity': 1} ) order_res = requests.post( f'{BASE}/orders/', headers=headers, json={'address': '测试地址', 'remark': '冒烟测试'} ) print('订单号:', order_res.json().get('order_no'))

这段脚本的价值在于它不依赖微信开发者工具,能直接判断后端是否健康。每次改完模型或接口字段,先跑一遍脚本,比反复点小程序页面快得多。脚本里每一步的返回值都可以打印出来核对,比如下单后检查库存有没有扣、订单总价和菜品单价乘数量是否对得上。

6.2 演示前的最后三关

第一关:清空脏数据。把测试产生的订单、购物车记录、临时图片全删掉,让评委看到的是一个干净的系统。第二关:真机预览而不是模拟器演示。模拟器里触屏事件、顶部导航栏高度、图片加载速度都和真机有差异,真机能跑通才算是真的能演示。第三关:检查 settings.py 的DEBUG=True和ALLOWED_HOSTS。如果后端部署到服务器上,DEBUG还开着,接口报错时会把堆栈信息直接暴露给用户,非常不专业。

从那以后我每次拿到毕设工程,都会强制走一遍“看模型 - 跑接口脚本 - 真机预览”的流程。只要订单、库存、图片这三个点不炸,答辩演示基本就能稳住,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询