简介:一套面向计算机专业毕业设计场景的家庭大厨微信小程序完整工程,后端基于Python Django,前端使用Vue,小程序端采用微信开发者工具,数据库选用MySQL,整体前后端分离,便于拆分学习与二次开发。系统覆盖用户与店铺注册登录、菜品信息及分类管理、购买菜品、订单行管理、管理员后台等模块,并贯穿可行性分析、功能设计与数据库设计的完整思路,从业务设计到数据落库形成闭环,工程参考价值较强。包内共718个文件,以Vue组件、Python后端脚本、微信小程序页面、图片样式资源为主,同时包含SQL数据库脚本、毕业论文文档、运行安装批处理以及MP4视频教程,压缩包整体44.78MB,目录结构与源码层级清晰,可快速定位前端页面、业务接口和数据库表。已有194人学习下载,适合需要完整毕设源代码、数据库设计文档和操作演示视频的开发者参考使用。
1. 家庭大厨小程序:先想清楚它到底在做什么
“微信小程序 + Django + Vue + MySQL 前后端分离开发的家庭大厨小程序”这类毕业设计,表面上是一个全栈课设题,实际动手后你会发现最花时间的不是写页面,也不是写接口,而是把小程序、Vue 后台、Django API、MySQL 四个进程真正拉通。真机才能调的接口、Vite 代理、数据库编码、跨域预检,任何一个都能卡住大半天。这篇文章按“数据模型 → Django → Vue → 小程序 → 联调避坑”的顺序,把每一步该做什么、参数怎么设、失败看哪里讲清楚。适合拿这套题目做毕设的同学,也适合想快速上手前后端分离项目实战的开发者参考。
2. 前后端分离的数据底座:表结构设计与数据库脚本导入
2.1 四个进程与一条数据链:为什么 Django 是唯一的 API 出口
家庭大厨小程序按前后端分离来拆,运行时一共有四个进程:微信小程序客户端、Vue 管理后台、Django 后端服务、MySQL 数据库。小程序和 Vue 后台都不直接连数据库,所有数据操作都走 Django 暴露的 REST API,这是前后端分离项目实战里最基本的一条约定。
数据流向是这样的:小程序用户打开首页,wx.request 请求 Django 的菜品列表接口,Django 用 ORM 查 MySQL,把结果序列化成 JSON 返回;管理员在 Vue 后台新增菜品,Vue 把表单数据 POST 给 Django 的管理接口,Django 再写库。两边共用同一套 API,只是接口权限不同。小程序端是普通用户权限,Vue 后台需要管理员登录态。
拿到这套题目的源码包之后,别急着打开代码,先按这个结构检查项目目录:后端 Django 工程、前端 Vue 工程、小程序原生工程、数据库脚本、论文。确认五个部分都在,再开始配环境。数据库脚本是第一个要跑通的东西,因为它决定了后面所有接口能不能查到数据。
2.2 数据模型拆分:家庭大厨需要的 7 张核心表
家庭大厨的业务围绕“菜谱”展开,数据模型按最小可用来拆,七张核心表就够了。用户表存微信用户信息,分类表存菜系或场景(家常菜、凉菜、汤羹、早餐等),菜品表存菜名、做法步骤、封面图,食材表存常用食材,菜品食材关联表解决“一道菜用多种食材、一种食材出现在多道菜”的多对多关系,收藏表和评论表分别记录用户行为和互动内容。
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user_profile | 小程序微信用户 | openid、昵称、头像、注册时间 |
| category | 菜品分类 | 分类名、排序值、图标 |
| recipe | 菜品主表 | 菜名、封面图、做法步骤、难度、时长、分类外键 |
| ingredient | 食材表 | 食材名称、常见单位 |
| recipe_ingredient | 菜品食材关联 | 菜品外键、食材外键、用量 |
| favorite | 收藏记录 | 用户外键、菜品外键、收藏时间 |
| comment | 评论表 | 用户外键、菜品外键、内容、评分 |
这张表结构参考了很多菜谱类小程序的设计,没有过度抽象,胜在简单直接。用户表单独建而不是直接复用 Django 自带的 User 表,是为了让 openid 和昵称头像这类微信字段有地方放,也避免和后台管理员的账号体系混在一起。菜品和分类是一对多,分类删除时菜品要处理,实际开发里一般做软删除或者限制删除。
2.3 从 SQL 脚本到 Django models:导入数据库脚本的三种姿势
题目里带了数据库脚本,最常见的形式是一个 .sql 文件。导入到 MySQL 的命令很简单,但很多人第一次执行就报错。先确保 MySQL 服务已经启动,然后用命令行导入:
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS familychef DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p familychef < database.sql第一条命令建库并指定 utf8mb4 字符集,第二条命令把脚本里的建表和插入语句执行到 familychef 库。这里有两个关键参数:utf8mb4 是必须的,因为小程序端会提交 emoji 表情和中文昵称,用老旧的 utf8mb3 会报 Incorrect string value;导入时如果出现乱码,在第二条命令前面加 --default-character-set=utf8mb4 重试。
如果你的机器上没装 mysql 命令行工具,也可以在 Navicat 或 MySQL Workbench 里直接运行 .sql 文件。还有第三种姿势:把 SQL 脚本转换成 Django migration。常见做法是先按脚本里的表结构写好 models,然后让 Django 的 makemigrations 生成迁移文件,再 migrate 到新库。这样做的优点是 models 和数据库强一致,缺点是原脚本里的初始数据要另外导入。我一般建议毕设阶段直接用原脚本建库,然后把 models 的表名和字段名对齐脚本,两边保持一致即可。
2.4 字符集与时间字段:两个最容易埋雷的建表参数
建表脚本里有几个参数值得单独说。第一个是时间字段的类型,推荐用 datetime 而不是 timestamp。timestamp 的范围到 2038 年,而且会跟着数据库时区走,Django 写入的 naive datetime 和 MySQL 的会话时区不一致时,查出来的时间可能差八个小时。用 datetime 存的是字面值,前端显示什么就是什么。
第二个是金额和评分字段。如果菜品要做价格或者评分,用 Decimal(5,2) 而不是 Float。Float 在 MySQL 里是近似值,评分 4.8 存进去可能变成 4.799999...,接口返回给小程序后前端没法直接做比较。Decimal 是精确值,排序和筛选都稳定。
第三个是排序值字段,category 表和 recipe 表里都建议加一个 sort_order 整型字段,默认 0。列表接口按 sort_order 升序、id 降序排列,管理员在 Vue 后台调整排序时只需要改这个数字,不用动表结构。很多毕设代码里省略了这个字段,导致后台不能调顺序,真做起来才发现不够用。
提示:导入脚本后,用 mysql 客户端执行 SHOW CREATE TABLE recipe; 检查一下表注释和自增主键有没有生成。有的脚本导出时带着 DROP TABLE IF EXISTS,执行前确认你库里的旧表可以删除。
3. Django 后端:把菜谱接口变成可调用的 REST API
3.1 项目脚手架:django-admin 与 app 拆分
Django 后端是整个系统的 API 出口,我习惯按业务拆成两个 app:一个负责用户认证,一个负责菜谱业务。先建项目和 app:
pip install django djangorestframework django-cors-headers PyJWT mysqlclient django-admin startproject familychef_backend cd familychef_backend python manage.py startapp user_auth python manage.py startapp recipe依赖里 mysqlclient 是让 Django 连 MySQL 的驱动,Windows 上经常装不上。如果 pip install 失败,可以用 pymysql 替代,在 settings.py 里加两行:
import pymysql pymysql.install_as_MySQLdb()但要注意,pymysql 的性能和兼容性在 Python 3.10+ 下偶尔有坑,毕设规模完全够用。app 拆成 user_auth 和 recipe,规则很简单:和微信登录、用户资料有关的放 user_auth,和分类、菜品、收藏、评论有关的放 recipe。不要把所有代码堆进一个 app,论文里画系统架构图的时候也更容易说清楚。
3.2 settings.py 里必须写对的四组配置
连接 MySQL、配置跨域、注册 app、设置 media 路径,这几组配置缺一个后端就跑不起来。下面是一份能直接用的配置片段:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', 'user_auth', 'recipe', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'familychef', 'USER': 'root', 'PASSWORD': '你的数据库密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", }, } } CORS_ALLOW_ALL_ORIGINS = True MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'CORS_ALLOW_ALL_ORIGINS 开发阶段设成 True 最省事,Vue 后台在 localhost:5173 请求 Django 的 8000 端口就不会被浏览器拦截。charset 指定 utf8mb4,否则 Django 写入的中文和 emoji 会乱码。MEDIA_ROOT 是上传图片落盘的目录,urls.py 里要追加 static 路由才能通过 URL 访问。
3.3 用 DRF 序列化器把 Recipe 变成 JSON
Django 的 ORM 查出来的是 QuerySet,不能直接返回给小程序。DRF 的 ModelSerializer 把数据转成 JSON,同时承担字段校验的工作。以菜品为例:
# recipe/serializers.py from rest_framework import serializers from .models import Recipe, Category class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = ['id', 'name', 'sort_order'] class RecipeListSerializer(serializers.ModelSerializer): category_name = serializers.CharField(source='category.name', read_only=True) class Meta: model = Recipe fields = ['id', 'name', 'cover_image', 'duration', 'difficulty', 'category_name'] read_only_fields = fields注意 category_name 这个字段:它从关联的 Category 表里取 name,序列化成字符串,小程序端拿到之后直接展示,不需要再发起一次分类查询。fields 里明确列出要返回的字段,不要在 Meta 里直接写 fields = 'all',否则密码、内部备注这类敏感字段很容易漏出去。
视图用 DRF 的 APIView 或者 ViewSet 都可以。毕设规模用 ViewSet 加 router 最省代码:
# recipe/views.py from rest_framework import viewsets from .models import Recipe, Category from .serializers import RecipeListSerializer, CategorySerializer class CategoryViewSet(viewsets.ReadOnlyModelViewSet): queryset = Category.objects.filter(status=True).order_by('sort_order') serializer_class = CategorySerializer class RecipeViewSet(viewsets.ReadOnlyModelViewSet): queryset = Recipe.objects.filter(status=True).order_by('-id') serializer_class = RecipeListSerializer def get_queryset(self): qs = super().get_queryset() category_id = self.request.query_params.get('category') if category_id: qs = qs.filter(category_id=category_id) return qsReadOnlyModelViewSet 只提供列表和详情两个动作,用在小程序端的公开接口上很合适,不需要暴露 POST 和 DELETE。get_queryset 里做的 category 筛选,对应小程序分类页点击某个分类后按分类 ID 拉取菜品列表的逻辑。
3.4 登录认证:小程序 code2session 与 JWT 签发
小程序端没有传统的账号密码,登录流程是:小程序调用 wx.login 拿到临时 code,传给 Django,Django 用 code 换 openid,再用 openid 签一个 JWT 返回给小程序。后续请求都把 JWT 放在 Authorization 头里。核心代码:
# user_auth/views.py import requests, jwt, time from django.conf import settings from rest_framework.views import APIView from rest_framework.response import Response from .models import UserProfile class WxLoginView(APIView): def post(self, request): code = request.data.get('code') if not code: return Response({'code': 400, 'msg': '缺少 code'}, status=400) url = 'https://api.weixin.qq.com/sns/jscode2session' params = { 'appid': settings.WX_APPID, 'secret': settings.WX_SECRET, 'js_code': code, 'grant_type': 'authorization_code', } resp = requests.get(url, params=params, timeout=5).json() if 'openid' not in resp: return Response({'code': 400, 'msg': '微信登录失败: %s' % resp.get('errmsg')}, status=400) openid = resp['openid'] user, _ = UserProfile.objects.get_or_create(openid=openid) payload = { 'user_id': user.id, 'openid': openid, 'exp': int(time.time()) + 7 * 24 * 3600, } token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256') return Response({'code': 0, 'token': token, 'user_id': user.id})code2session 接口固定在 api.weixin.qq.com,三个参数中 appid 和 secret 会暴露给后端,不要写进小程序前端代码。请求失败时 resp 里会带 errcode 和 errmsg,常见的是 40029(code 无效)和 40163(code 已使用),原因是同一个 code 只能换一次 openid,调试时重复请求同一个 code 就会报错。JWT 的 exp 设成七天,小程序的 token 过期后自动重新登录即可。
3.5 分页、图片上传与查询参数:接口细节的默认值
列表接口在数据量小的时候看不出问题,一旦导入几百条测试数据,没有分页会让小程序端卡顿。在 settings.py 里配置全局分页:
REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, }这样小程序端请求 /api/recipes/?page=1 会返回 count、next、previous、results 四个字段,前端用结果中的 results 渲染列表。图片上传接口放在 Vue 管理后台这边,用 DRF 的 APIView 接收文件:
class UploadImageView(APIView): def post(self, request): file = request.FILES.get('file') if not file: return Response({'code': 400, 'msg': '没有文件'}) ext = os.path.splitext(file.name)[-1].lower() filename = str(int(time.time())) + ext with open(os.path.join(settings.MEDIA_ROOT, filename), 'wb') as f: for chunk in file.chunks(): f.write(chunk) return Response({'code': 0, 'url': settings.MEDIA_URL + filename})文件名用时间戳重新生成,避免中文名和重复名带来的一堆乱码问题。file.chunks() 是逐块读取,适合图片文件。生产环境要考虑文件大小限制,Django 默认没有限制,你可以在 Nginx 层或者中间件里加。Vue 后台上传成功后拿到 URL,把 URL 存到 recipe 表的 cover_image 字段。
4. Vue 管理后台:用 Element Plus 把菜品管理页面搭起来
4.1 用 Vite 搭 Vue3 后台:为什么不用 Vue2
管理后台面向管理员,功能是菜品的增删改查、分类管理、用户列表、数据统计。前端框架直接选 Vue 3 + Vite + Element Plus,理由很简单:Vite 冷启动和热更新比 Vue CLI 快一个量级,后台开发中来回改表格和表单的体验差距非常大。
npm create vite@latest familychef_admin -- --template vue cd familychef_admin npm install npm install element-plus axios vue-router安装完成后,在 main.js 里全局注册 Element Plus。Vue2 的项目不建议选了,Element UI 已经停止维护,新写的毕设用 Vue3 生态更合理,论文里写“基于 Vue3 的响应式数据绑定和组合式 API”也比旧版本有话说。
4.2 axios 封装与路由跳转:统一处理 401 和权限
后台的请求都要带管理员 token,而且登录过期时要跳回登录页。axios 封装成独立模块,拦截器统一处理这些逻辑:
// src/api/index.js import axios from 'axios' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 15000, }) request.interceptors.request.use(config => { const token = localStorage.getItem('admin_token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('admin_token') router.push('/login') } return Promise.reject(error) } ) export default requestbaseURL 用 /api 而不是 http://127.0.0.1:8000,是为了配合 Vite 代理。后端所有接口前缀统一是 /api,前端请求路径写 /api/recipes/,代理会把请求转发到 Django 真实地址。401 拦截做的是统一跳登录,这样任何接口报未授权都会回到登录页,不用在每个页面里重复写判断。路由跳转用 vue-router,登录页和主布局分开,主布局里用 meta 字段控制哪些页面需要管理员权限。
4.3 菜品管理页:表格、弹窗、上传三件套
菜品管理页是后台最核心的页面,结构是:顶部搜索栏,中间 el-table 展示菜品列表,按钮触发 el-dialog 弹窗新增或编辑,弹窗里放 el-form 表单和 el-upload 图片上传。核心模板简化如下:
<template> <div> <el-button type="primary" @click="openDialog()">新增菜品</el-button> <el-table :data="recipeList" border stripe> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="菜名" /> <el-table-column prop="category_name" label="分类" /> <el-table-column label="封面图"> <template #default="{ row }"> <el-image :src="row.cover_image" style="width: 60px; height: 60px" /> </template> </el-table-column> <el-table-column label="操作"> <template #default="{ row }"> <el-button size="small" @click="openDialog(row)">编辑</el-button> <el-button size="small" type="danger" @click="deleteRecipe(row.id)">删除</el-button> </template> </el-table-column> </el-table> <el-dialog v-model="dialogVisible" :title="form.id ? '编辑菜品' : '新增菜品'"> <el-form :model="form" label-width="80px"> <el-form-item label="菜名"> <el-input v-model="form.name" /> </el-form-item> <el-form-item label="分类"> <el-select v-model="form.category" placeholder="选择分类"> <el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" /> </el-select> </el-form-item> <el-form-item label="封面图"> <el-upload action="/api/upload/image/" :headers="uploadHeaders"> <img v-if="form.cover_image" :src="form.cover_image" class="cover" /> </el-upload> </el-form-item> </el-form> <template #footer> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="saveRecipe">保存</el-button> </template> </el-dialog> </div> </template>el-upload 的 action 直接写 /api/upload/image/,走的是代理,不需要写本机 IP。uploadHeaders 要把 token 带上,否则 Django 端区分不了上传者。编辑和新增共用一个 dialog,通过 form.id 判断是 PUT 还是 POST 请求。保存成功后重新拉一次列表,不要手动修改表格数据,保证页面和数据库一致。
4.4 vite 代理与跨域:本地联调的最后一公里
后台开发时页面跑在 5173 端口,Django 跑在 8000 端口,浏览器直接请求必然跨域。虽然第 3 章里配置了 CORS_ALLOW_ALL_ORIGINS,前端还是建议走代理,这样生产环境和开发环境前端代码不用改地址。在 vite.config.js 里配置:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, }, '/media': { target: 'http://127.0.0.1:8000', changeOrigin: true, }, }, }, })/api 和 /media 都要代理。前者转发接口请求,后者让上传的图片在 <el-image> 里能正常显示。这里踩坑最多的就是只代理了 /api,图片全挂在 /media 下,结果页面裂图。changeOrigin 设为 true,是因为目标服务器拿到请求头时看到的 Host 是 127.0.0.1:8000,避免 Django 的 ALLOWED_HOSTS 校验拒绝请求。生产环境部署时,Nginx 里做同样的 location /api 和 location /media 转发即可。
5. 小程序端联调与避坑清单:从 wx.request 到真机调试
5.1 wx.request 封装:把 Promise 和 token 管理一起解决
微信小程序的 wx.request 默认是回调风格,多个接口串行请求时代码很难看。封装成 Promise 风格,同时把 token 注入和错误提示统一处理:
// utils/request.js const BASE_URL = 'http://127.0.0.1:8000' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '', }, success(res) { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data) } else if (res.statusCode === 401) { wx.removeStorageSync('token') wx.navigateTo({ url: '/pages/login/login' }) } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res.data) } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }) reject(err) }, }) }) } module.exports = { request }BASE_URL 在开发阶段直接用 127.0.0.1:8000,手机真机调试时要把这个地址改成电脑在局域网内的 IP,否则手机连不上。这里有个常见的翻车点:微信开发者工具里勾选了“不校验合法域名”后,请求能通;真机预览时必须在小程序后台配置 request 合法域名,域名必须备案并且是 HTTPS。没有正式域名时,可以用开发者工具的“真机调试”功能临时绕过,但给评委演示时还是建议用工具模拟器。
5.2 首页与分类页:setData 的最佳实践
首页布局一般是顶部分类导航,下面菜品瀑布流。页面加载时并发请求分类列表和推荐菜品,数据回填用 setData:
// pages/index/index.js const { request } = require('../../utils/request.js') Page({ data: { categories: [], recipes: [], activeCategory: 0, page: 1, hasMore: true, }, onLoad() { this.loadCategories() this.loadRecipes() }, async loadCategories() { const res = await request('/api/categories/') this.setData({ categories: res.results }) }, async loadRecipes() { const category = this.data.activeCategory const url = category ? '/api/recipes/?category=' + category : '/api/recipes/' const res = await request(url) this.setData({ recipes: res.results }) }, onCategoryTap(e) { const id = e.currentTarget.dataset.id this.setData({ activeCategory: id, page: 1 }, () => { this.loadRecipes() }) }, })setData 的第二个参数是回调函数,在数据更新完成后执行。分类切换时先把 activeCategory 和 page 重置,再加载菜品列表,避免旧列表残留。小程序端 setData 有性能问题,一次性传入大数组会明显卡顿。接口做了分页后,每次只传一页数据,页面往下滚动时再加载下一页。图上不到 30 张菜品图时基本流畅。
5.3 避坑清单:真机图片不显示、中文乱码、401、CORS、版本不一致
现象一:开发者工具里图片正常,真机上全部裂图。原因:图片地址是 http://127.0.0.1:8000/media/xxx.jpg,真机访问这个地址指向的是手机自己,不是电脑。解决:把 BASE_URL 和图片 URL 前缀都改成电脑局域网 IP,并且让 Django 的 ALLOWED_HOSTS 加上这个 IP。
现象二:Django 接口返回的中文变成一串问号。原因:MySQL 连接没指定 utf8mb4,或者数据库表本身就是 latin1。解决:先看数据库 SHOW VARIABLES LIKE 'character_set_server';,确认是 utf8mb4;再把 Django settings 里 DATABASES 的 OPTIONS 加上 charset,改完重启 Django。
现象三:小程序登录后第一次请求成功,过一会儿全部返回 401。原因:JWT 过期或者 token 没带在 Header 里。解决:检查 request.js 里 Authorization 的拼写,Bearer 后面有空格;检查登录接口返回的 token 是否真的存进了 Storage;如果是在调试,把 JWT 的 exp 调到 30 天,省得反复登录。
现象四:Vue 后台请求 Django 报 CORS 错误,network 面板显示 OPTIONS 请求 500。原因:Django 的 CORS 配置放在中间件最后,或者请求头里带了自定义字段但没有允许。解决:corsheaders.middleware.CorsMiddleware 必须放在 CommonMiddleware 前面;后端设置了 ALLOWED_HEADERS 的,把 Authorization 加进去。
现象五:按视频教程操作却一直报错,命令找不到包。原因:教程录制时的 Python 版本和依赖版本和你本机不一致。解决:先看 requirements.txt 有没有版本号,有就严格按着装;没有的话,Python 优先用 3.8 到 3.10,Node 用 16 以上,Django 用 3.2 或 4.x 都行,但不要把 Django 5 和一个老项目硬凑。
提示:微信开发者工具右上角的“详情 → 本地设置”里有“不校验合法域名”选项,开发阶段必须勾上。真机预览时如果请求失败,点开调试器的 Network 面板看具体报错,比盲猜快得多。
6. 优化与验收:让这套毕业设计在答辩时站得住
答辩演示最怕的是现场接口超时或者图片加载不出来。我习惯在答辩前做一次完整的验收:清空数据库,重新导入 database.sql,启动 Django 和 Vue,小程序重新编译,按“首页 → 分类 → 详情 → 收藏 → 管理后台新增菜品 → 小程序刷新看到新菜”的路径走一遍。这个流程走通,系统的主链路就没有问题了。
给接口做一次简单的压测,确认系统能扛住并发访问,也是论文里可以写的亮点。用系统自带的 ab 命令:
ab -n 500 -c 50 http://127.0.0.1:8000/api/recipes/-n 500 表示总共 500 个请求,-c 50 表示 50 个并发。跑完后看 Requests per second 和 Failed requests 两项。能跑到每秒几十个请求就足够应付毕设场景了。如果压测时失败率很高,先看 MySQL 的最大连接数,再看 Django 是不是 DEBUG=True。DEBUG 模式会吞掉错误并拖慢请求,部署或压测前务必关掉。
日志是排查线上问题的最后一块拼图。Django 默认只在控制台打日志,文件日志要自己配。在 settings.py 里加一个 FileHandler,把 ERROR 级别以上的日志写到文件,答辩演示时出了问题可以直接翻日志定位,比在黑匣子里猜快得多。我自己的习惯是把日志文件和 media 目录都放在项目根目录外,避免源码包和生成文件混在一起。
这套技术栈的边界也值得说一句:Vue 后台适合单管理员维护几百条菜品数据,小程序端适合日活几千的量级。如果数据量到几十万条,MySQL 加索引比换组件库更紧急;如果要做实时推送,Django 的 Websocket 是更能拉开差距的方向。
这套题目我做过不止一次,每次拿到新的源码包第一件事都是先跑数据库脚本,再跑后端,最后调前端。印象最深的一次就是忘配 MEDIA 路径,Vue 里上传图片一直返回 404,最后发现 urls.py 少了 static 路由。希望你这次少踩这些坑,希望帮到你。
本文还有配套的精品资源,点击获取