news 2026/9/29 15:45:37

基于Python+Vue的美食分享系统:Django与Flask混合架构实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python+Vue的美食分享系统:Django与Flask混合架构实战解析

做项目这些年,我有个挺深的体会:一个看着挺完整的系统,拆开来往往没那么多高深东西,能跑通上线,靠的全是细节上的把关和踩坑后的复盘。这次写的是一个基于Python和Vue的美食分享系统,技术栈涵盖了Pycharm、django和flask。说实话,一开始看到这个项目的时候,我第一反应是有点困惑的——为什么一个系统里会同时出现django和flask?这不就是别人常说的“重复造轮子”吗?但真正把项目做完之后,我才发现这种组合本身就是一种很实在的工程决策。今天就把这个项目的完整搭建过程、技术取舍、以及我在实际开发中碰到的问题,从头到尾整理出来。不管你是正在用django写后端、刚接触Vue想做前后端分离,还是单纯想看看一个美食分享系统该怎么从零落地,这篇文章应该都能给你一些有用的参考。

1. 为什么一个系统里会同时出现django和flask

先回答开头那个问题。这个美食分享系统的核心后端确实是基于django做的,因为项目里有用户注册登录、菜谱发布、评论互动、收藏点赞,还有后台管理这类经典且有状态的功能模块。django的ORM、Admin后台、表单校验、认证体系这些东西在开发这种业务系统的时候,优势非常明显,能帮我把大量重复的CRUD逻辑直接压到最低。

那flask在这个项目里干了什么呢?答案是推荐模块和部分轻量级接口。美食分享系统里有一个高频功能:基于用户点赞和浏览记录,返回一个“你可能喜欢”的菜品列表。这个推荐算法本身是个纯计算服务,不依赖数据库事务和用户体系,我希望能单独把它拆出来跑。flask作为微框架足够轻,写一个推荐接口只需要几十行代码,启动压力小,部署的时候也可以相对独立。而django负责的是重业务逻辑和数据库操作,这样两个框架各管一摊,在工程上反而清晰很多。

这种组合在真实项目中并不少见。很多团队会在django之外挂一个flask或者fastapi服务,专门跑算法、爬虫回调或者webhook之类的轻逻辑。拿我来说,做一个简单的类似度计算,只需要在flask里构建一个食材标签向量,再用余弦相似度去匹配,整个过程不需要经过django的模型层。这种架构让我在写业务接口的时候完全不用被推荐服务的代码干扰,两套逻辑互不污染。

再说回工具链。Pycharm是这个项目的主开发IDE,两个框架的代码都在同一个项目目录里管理。Pycharm对django和flask都有模板支持,创建项目、启动服务、Debug的时候非常顺手,尤其是它对数据库查询的调试能力,在排查ORM生成SQL的问题时特别管用。后面我会单独讲Pycharm环境的配置细节,这里面坑其实也不少。

2. 环境准备里容易被忽略的几个关键点

2.1 Python版本和虚拟环境不要凭感觉选

我见过太多人一上来直接装最新版Python,然后项目跑不起来就开始怀疑代码。这个美食分享系统我建议使用Python 3.10.x,原因很简单:django的LTS版本在3.10上兼容性最稳,flask的所有常用扩展也都能正常安装,不会碰到某些依赖库预编译wheel版本缺位的问题。

虚拟环境我强烈建议用虚拟环境,不要图省事直接全局安装。因为django和flask的某些依赖(比如Werkzeug)可能会在版本上打架。两个框架并存的时候,锁版本比什么都重要。我在项目根目录下建了一个venv目录,激活后先统一把基础依赖装齐,后面再分别装django和flask的扩展包。

另外一个容易忽略的问题是Pycharm的Python解释器指向。很多人创建项目的时候选了解释器,但后面用命令行执行迁移命令的时候,用的却是系统全局的Python环境,结果就是解释器版本不一致导致ORM查询报错。这里有个小技巧:在Pycharm的终端里,激活虚拟环境后,先执行一下python --version,再执行python -c "import django; print(django.__version__)",确认当前终端里能正确导入django,再跑迁移命令。

2.2 Django项目结构怎么组织才不混乱

在Pycharm里新建一个django项目后,默认生成的结构其实还算清晰,但对于美食分享系统来说,直接把代码写死在django项目目录里后续会乱。我选择把核心业务拆成几个子app,每个app负责独立的业务域:

  • accounts:用户注册、登录、个人信息
  • posts:菜谱发布、菜品列表、菜品详情
  • comments:评论和回复
  • favorites:收藏和点赞
  • recommend:调用flask服务获取推荐结果

每个子app里我建议都自带独立的 models.py、views.py、serializers.py。这样后续维护的时候,改评论逻辑不用去翻菜谱的代码,对新手理解项目结构也有很大帮助。

2.3 Vue环境配置的常见绊脚石

Vue这边的环境配置也是新手的重灾区。热搜词里都提到vue安装及环境配置,这个确实值得单独说说。我用的Vue版本是Vue 3,配合Vite构建工具,而不是Vue 2的webpack方式,原因后面会讲。需要先装Node.js,版本最好在16以上,装完之后用npm命令安装Vite脚手架:

npm create vite@latest food-frontend -- --template vue cd food-frontend npm install npm run dev

这里有几个容易栽跟头的地方:

第一是npm镜像特别慢,甚至安装一半卡死。解决办法是换成淘宝镜像,之前踩过好多次。设置方式很简单:

npm config set registry https://registry.npmmirror.com

第二是Node版本问题。Vite 3以上版本要求Node 14.18+或者16+,如果你的机器装的是旧Node版本,启动dev服务器的时候会直接报错,提示版本不支持。这个层面没什么好讨论的,装新版本就好。

第三是Pycharm和Vue的配合问题。Pycharm虽然对前端代码也有支持,但Vue单文件组件的语法高亮和格式化,我建议还是单独用VS Code来写前端,Pycharm专心管Python后端。分工明确,体验会好很多。如果你非要用Pycharm写Vue,需要装Vue.js插件,同时把Vite的dev-server端口(默认5173)跑在本地,然后用浏览器直接访问,这样也可以调试。

3. Django后端的核心功能实现与踩坑笔记

3.1 用户注册登录:从自定义User模型开始

做美食分享系统,用户账号体系是第一个绕不开的模块。我强烈建议你在项目一开始就自定义用户模型,哪怕表面上看起来用django的默认User也行。因为默认的User模型没有手机号、头像、个人简介这些字段,如果项目跑到一半再想加字段,就要做数据库迁移的操作,非常折腾。

自定义用户模型最简单的方式是继承AbstractUser:

from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) phone = models.CharField(max_length=11, blank=True, null=True) bio = models.TextField(max_length=200, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'users'

然后在settings.py里指定:

AUTH_USER_MODEL = 'accounts.User'

这里有个很重要的坑,如果你在之后做了数据库迁移,再改AUTH_USER_MODEL,会非常痛苦,因为django的权限表和用户表已经关联了。所以这个决策必须在项目最开始就做好。

注册接口我选择了基于jwt的认证方案,因为前端是Vue做的单页应用,用token认证比session更自然。登录接口返回access token和refresh token,前端存起来,后续请求带上Authorization头就行。jwt库用的是djangorestframework-simplejwt,配置起来不复杂,但这个过程中也踩了几个坑,我一个个说。

3.2 菜谱发布:图片上传和富文本处理的方案

菜谱发布是系统的核心模块。用户需要填菜名、食材列表、制作步骤,还可以上传成品图。图片上传这块,django的ImageField本身非常好用,但是需要配合MEDIA_ROOT和MEDIA_URL来配置存储路径。

在settings.py里加上:

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

然后在项目的urls.py里处理静态媒体文件:

from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

上传图片的时候,前端用FormData提交,后端接收文件对象,存到指定目录,然后把文件的URL存进数据库。这一步要看清楚的一点是,ImageField保存的是相对路径,真正线上访问的时候需要拼接完整的域名。开发阶段直接返回request.build_absolute_uri(instance.image.url)就行。

食材列表这种列表型数据不要用逗号拼接存字符串,虽然实现简单,但后面想按食材搜索的时候就后悔了。我建了一个食材表,菜谱和食材之间用多对多关联,这样用户在搜索的时候就可以直接通过食材名反向查询菜谱,效率高得多。这就是django的QuerySet和ORM的便利之处。

3.3 Django执行查询,特别是删除对象的注意事项

在热搜词里反复出现的“django执行查询-删除对象”,这是新手几乎都会踩的坑。django删除对象有两种方式:

# 方式一:删除单个实例 food = Food.objects.get(id=1) food.delete() # 方式二:批量删除 Food.objects.filter(category_id=3).delete()

删除单个对象的时候没问题。但批量删除的时候要特别注意,django默认的delete()方法是会遍历所有关联对象,把这些对象也一并删掉的。比如我删除一个分类对象的时候,属于这个分类的所有菜谱会被级联删除。如果你的业务逻辑不想要这种级联效果,可以在外键里设置on_delete=models.SET_NULL或者on_delete=models.PROTECT。

还有一个小细节是get()方法如果查不到对象会抛DoesNotExist异常,查到多个会抛MultipleObjectsReturned异常,所以实际开发中更稳妥的写法是:

food = Food.objects.filter(id=1).first() if food is not None: food.delete()

用filter().first()代替get(),就不会因为没查到数据而中断服务了。这个习惯建议早点养成,能帮你少接很多报错。

3.4 Django的Admin后台自定义:轻松管理菜谱数据

django自带的后台管理是这个框架绕不开的亮点之一。美食分享系统里,运营需要审核用户发布的菜谱,所以后台的可用性很重要。默认的Admin界面虽然信息全,但看起来不够友好,而且关联关系、图片预览这些都需要额外配置。

我重写了一个简单的FoodAdmin:

from django.contrib import admin from .models import Food, Ingredient class IngredientInline(admin.TabularInline): model = Food.ingredients.through extra = 1 class FoodAdmin(admin.ModelAdmin): list_display = ('title', 'user', 'category', 'created_at') list_filter = ('category', 'created_at') search_fields = ('title', 'ingredients__name') inlines = [IngredientInline] fieldsets = ( ('基本信息', { 'fields': ('title', 'cover', 'category', 'user') }), ('内容详情', { 'fields': ('steps', 'ingredients') }), ) admin.site.register(Food, FoodAdmin)

后台这里有一个典型的问题:搜索字段如果关联到多对多字段(比如食材),django会在后台搜索的时候做JOIN查询,数据量大的时候会有性能开销。不过普通项目的数据量级完全不用过度优化,等真正出现性能瓶颈再考虑Elasticsearch或者缓存也不迟。

4. Vue前端:搭建美食分享界面的核心逻辑

4.1 Vue 3和Vite的组合为什么更香

刚接触Vue时,很多人还在用Vue 2和vue-cli,但新项目我觉得完全没有理由再选Vue 2了。Vue 3的组合式API(Composition API)让逻辑复用变得舒服很多,尤其是同一个功能需要用到多个状态的时候,不用再为了一个mixin绞尽脑汁。Vite启动速度比webpack快太多了,因为它是用浏览器原生ES模块加载方式,开发阶段几乎秒开。

Vue 3的工程里,路由用vue-router 4,状态管理用pinia,分别替代了vue-router 3和vuex。这个组合是现在Vue生态的标准答案。pinia相比vuex的明显优势是类型提示好,store定义简单,不需要写各种mutations、actions的样板代码。

4.2 菜谱列表页和详情页的路由设计

首页展示菜谱列表,详情页展示菜谱内容,这个交互流程可以用vue-router的动态路由来搭。

import { createRouter, createWebHistory } from 'vue-router' import HomePage from '../views/HomePage.vue' import FoodDetail from '../views/FoodDetail.vue' const routes = [ { path: '/', name: 'home', component: HomePage }, { path: '/food/:id', name: 'food-detail', component: FoodDetail } ] const router = createRouter({ history: createWebHistory(), routes }) export default router

详情页里通过route.params.id获取菜品ID,再调用后端接口拿到详细数据。在这个过程中,有一个很典型的坑:在Vue 3里,如果同一个路由组件内只变化了参数(比如从/food/1切到/food/2),组件实例会被复用,onMounted的请求不会重新执行。解决办法是在组件里监听路由变化,或者用watch重新拉取数据,不然切换详情页的时候页面内容不会更新。

import { watch } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() watch(() => route.params.id, (newId) => { fetchFoodDetail(newId) }, { immediate: true })

4.3 与后端交互的API封装方式

前后端分离的项目,API请求模块的封装决定了后续开发的舒服程度。我用axios做请求库,封装了一个request实例,统一处理baseURL、超时时间和请求拦截。

import axios from 'axios' import { useUserStore } from '../stores/user' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { // 跳转登录页 } return Promise.reject(error) } )

这里我建议配置Vite的proxy代理,开发阶段把/api开头的请求全部转发到django后端,这样就不存在跨域问题了。具体在vite.config.js里:

export default { server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } } }

这个方案比在后端配CORS要干净很多,生产环境再用nginx统一代理,整个链路就顺了。

4.4 图片懒加载和列表优化

美食类网站的列表页通常会有大量图片,如果不做懒加载,页面首次加载的图片请求数会非常多,体感会很差。我用的方案是第三方库v-lazy,同时也比较推荐原生IntersectionObserver来实现,其实根本不复杂。

const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target img.src = img.dataset.src imageObserver.unobserve(img) } }) }) // 在每个img元素上设置>app.directive('lazy', { mounted(el, binding) { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { el.src = binding.value observer.unobserve(el) } }) observer.observe(el) } })

然后在模板里用v-lazy="item.cover_url"替代v-bind:src就行。列表接口做分页或者无限滚动的时候,配合这个懒加载指令,滚动浏览的体验就和正常App差不多了。

5. 热搜词里藏着的坑:m3u8播放、websocket推送、flask部署

5.1 Vue播放m3u8视频的兼容性处理

这里有个挺有意思的现象,美食分享系统本身并不需要做视频播放,但很多搜"Vue播放m3u8"的人其实都是在折腾各类系统里的视频功能。项目里如果要嵌入做菜过程的视频,视频源一般会走m3u8格式,这样的话就要在Vue里处理HLS流播放。

m3u8本质是Apple的HTTP Live Streaming协议。普通浏览器的video标签不会直接支持m3u8格式,只有Safari能原生播放。所以在Vue项目里播放m3u8,最简单的方案是用hls.js这个库。

import Hls from 'hls.js' if (Hls.isSupported()) { const video = document.getElementById('video-player') const hls = new Hls() hls.loadSource(videoUrl) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play() }) }

如果是在Chrome和Firefox上,这段代码可以直接跑通。如果浏览器不支持hls.js(比如老的IE),就fallback到原生video标签播放。对于美食教程这种短视频内容,更建议直接在服务端转成mp4格式再输出,处理起来省心很多。

这个功能我另外提一句,如果我做的是视频分享,转码这块会特别吃服务器资源,不做点播平台的话完全没必要上m3u8,老老实实用通用格式反而轻量。

5.2 Django WebSocket:后端有数据时前端自动推送

热搜词里有一句很长的“django python websocket实现后台有数据前端推送”,看到这个关键词,基本可以判断是在做一个实时通知功能,我的美食分享系统也有类似需求:用户发布菜谱之后,他的粉丝收到一条新作品通知,这个场景用WebSocket正合适。

Django里做WebSocket推荐用Django Channels,它在django的WSGI之外加了一层ASGI支持,可以和传统HTTP接口并存。安装:

pip install channels channels-redis

然后在项目settings.py里注册channels并配置ASGI application:

INSTALLED_APPS = [ 'daphne', 'channels', # ... ] ASGI_APPLICATION = 'food_project.asgi.application' CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, }

在asgi.py里配置:

import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from .websocket_urls import websocket_urlpatterns os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'food_project.settings') application = ProtocolTypeRouter({ 'http': get_asgi_application(), 'websocket': AuthMiddlewareStack( URLRouter(websocket_urlpatterns) ), })

WebSocket连接的路由可以定义在单独的文件里:

from django.urls import path from .consumers import NotificationConsumer websocket_urlpatterns = [ path('ws/notifications/', NotificationConsumer.as_asgi()), ]

Consumer里的核心逻辑,就是当某个用户触发新菜谱发布时,通过channel layer向对应粉丝组推送消息:

class NotificationConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name = f"user_{self.scope['user'].id}" await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def receive(self, text_data): # 接收前端发来的消息,根据业务需求处理 pass async def send_notification(self, event): await self.send(text_data=event['content'])

在view里发消息的时候,用async_to_sync(channel_layer.group_send)就能把通知推给对应群组。这个过程中最需要注意的就是Redis服务必须提前启动,不然channel layer连不上会报错。另外如果用的是Windows系统,Redis装起来比Linux麻烦,这一点在项目里需要提前确认好。

5.3 Flask部署:别再守着开发服务器不放

flask在系统里的角色是推荐服务,我单独部署在一个端口(比如5001)上。但很多人在部署flask时会直接跑app.run(),默认启动的是Werkzeug开发服务器,这种服务器性能非常有限,完全不适合线上环境。

建议部署flask时用gunicorn,Linux系统下的经典搭配。项目根目录写一个wsgi.py:

from app import app as application if __name__ == "__main__": application.run()

然后通过gunicorn启动:

gunicorn -w 4 -b 0.0.0.0:5001 wsgi:application

-w 4表示启动4个worker进程,具体的数量一般根据CPU核数来定,推荐是2 * CPU核数 + 1的公式。如果是Windows服务器环境,gunicorn装不了,可以用waitress,也是一个比较成熟的纯Python WSGI服务器,配置方式类似。

线上推荐服务的调用,是django后端通过requests库或httpx去请求flask的/recommend接口,拿到菜品列表,再返回给前端。这个链路因为多了一层HTTP调用,做超时控制和异常兜底就特别重要:

import httpx try: response = httpx.get( "http://127.0.0.1:5001/recommend", params={"user_id": user_id}, timeout=2.0 ) data = response.json() except httpx.RequestError: # 推荐服务挂了,返回热门菜谱兜底 data = Food.objects.order_by('-view_count')[:6]

这里有个经验可以分享:和算法服务交互时,宁可拿一个旧的热门列表顶上去,也不能让用户的请求因为推荐服务不可用而失败。线上系统里暴露出“推荐挂了”比“页面打不开”要好得多。

5.4 另外一个框架相关的隐藏知识点:flask如何绑定到网页元素

热搜词里有个“flask如何绑定到网页元素”,初看有点莫名其妙,但结合上下文就明白了——很多刚开始学flask的人,不知道怎么把后端的Python变量传到前端HTML里渲染。

在flask中,这个问题的核心是Jinja2模板引擎。后端通过render_template传递变量:

from flask import Flask, render_template app = Flask(__name__) @app.route('/') def index(): food_list = ["宫保鸡丁", "麻婆豆腐", "回锅肉"] return render_template('index.html', foods=food_list)

在模板文件里就能这样渲染:

<ul> {% for food in foods %} <li>{{ food }}</li> {% endfor %} </ul>

不过要说明白,如果项目前端是Vue的话,flask就纯当API服务用,返回JSON数据,根本不需要模板渲染这一步。这就是为什么我前面强调要区分清楚两个框架的职责边界——混用模板渲染和前端框架是最容易让项目变成一锅粥的地方。

6. 前后端联调阶段的高频问题和解决记录

6.1 跨域:不是所有场景都需要后端开CORS

做前后端分离项目,跨域问题是逃不开的。开发阶段,我用Vite的proxy代理解决了大部分接口的跨域。这种方法的好处是,前端请求的是本地相对路径/api/xxx,由Vite开发服务器转发到后端,浏览器根本感知不到跨域,因此也不需要后端配CORS中间件。

但是如果你不是用Vite,或者前端请求的是别的端口域名,那后端就需要配CORS了。django里用django-cors-headers这个库,安装后把它加到INSTALLED_APPS,中间件也需要注册:

MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # ... ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", ]

这个配置只允许来自5173端口的前端页面访问接口。如果配置不正确或者漏了,浏览器控制台会明确报跨域错误,开发者很轻松就能定位到。

6.2 Token存储在哪个位置更安全

登录后前端拿到的JWT token到底应该放localStorage还是sessionStorage,甚至放cookie?这个问题我专门纠结过,也是踩坑比较多的地方。

localStorage的缺点是万一被XSS攻击,攻击者可以拿到token伪造请求,风险比较大。但如果完全不用token,用HttpOnly cookie,前后端分离的CSRF防护会变得复杂。实际项目中我用的是一个折中方案:把token放在内存(pinia store)里,页面刷新的时候调一个/auth/refresh接口重新获取token。这个方案安全性相对好一些,但实现复杂度稍微高一点。

如果你为了省事直接把token放localStorage,也不是不行,但前提要保证前端对XSS的防范做到位,尤其是不能把用户评论内容用v-html直接渲染。我在美食分享系统里,所有用户的菜谱步骤和评论内容,都统一走纯文本渲染,这样能把XSS风险降到最低。

6.3 接口返回结构的统一设计

前后端联调最费时间的还真不是接口写不出来,是每个接口返回的数据结构不一样,前端解析起来东一块西一块。在项目里我约定了统一的返回格式:

{ "code": 0, "message": "success", "data": {} }

所有接口都按这个格式返回。错误情景下,code是非0值,message描述错误信息。后端封装一个统一响应函数:

def ok(data=None, message="success"): return JsonResponse({ "code": 0, "message": message, "data": data }) def fail(message="error", code=1): return JsonResponse({ "code": code, "message": message, "data": None })

前端axios的响应拦截器里统一判断code的数值,这样只要一层的逻辑就能处理所有异常分支。很多新手在开发接口时,一个接口返回数组,另一个接口直接返回对象,前端代码里全是一个个的特判,越到后面越难维护。

7. 数据表设计和查询性能的实操建议

7.1 核心表关系和字段规划

美食分享系统最核心的几张表:用户表、菜谱表、食材表、菜谱-食材关联表、评论表、收藏表、点赞表。

用户表前面已经说过了,这里重点看菜谱表:

class Food(models.Model): title = models.CharField(max_length=100) cover = models.ImageField(upload_to='covers/') steps = models.TextField() user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='foods') category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True) view_count = models.PositiveIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True)

创建索引的问题多念叨一句:外键字段和经常查询的字段要加db_index=True,默认情况下外键本身就会建索引,但比如view_count按人气排序、created_at按时间排序这些场景,也都值得建一下。数据量小的时候感知不到,但等积攒了几万条菜谱数据之后,索引差别的效果就明显出来了。

7.2 减少N+1查询的关键写法

在前端展示菜谱列表的时候,需要同时显示发布者头像和分类名称。如果代码这样写:

foods = Food.objects.filter(status=1).order_by('-created_at')[:20]

那么当遍历这个列表,每访问一个food的user或者category属性时,django的ORM又会执行一次额外的数据库查询。这个就是N+1查询问题,列表渲染20条,就会产生1+20条SQL。

解决办法是用select_related和prefetch_related:

foods = Food.objects.filter(status=1) \ .select_related('user', 'category') \ .prefetch_related('ingredients') \ .order_by('-created_at')[:20]

外键关联用select_related,多对多关联用prefetch_related。这样拿回列表之后,访问关联对象不会再触发SQL查询。这两个方法看着是ORM的基础知识点,但实际开发中能压掉一大半查询量。

7.3 分页方案:接口和前端配合好

美食列表的分页,我用的是最传统也最稳妥的页码分页。django REST framework自带的PageNumberPagination:

class FoodPagination(PageNumberPagination): page_size = 12 page_size_query_param = 'page_size' max_page_size = 50

前端在滚动到底的时候,把当前页码加1,继续请求下一页数据。这里要注意的是,返回数据里的total总数和current_page信息,一定要从接口里带回来,前端判断“没有更多数据”的条件就靠这些。

8. 部署上线:Linux服务器上从头跑通整个系统

8.1 Django项目的线上启动方式

在Linux服务器上部署django,不能说用python manage.py runserver就直接应付过去,那是开发环境的使用方式,性能不够,也不安全。我用的是gunicorn配合nginx的方式,和flask推荐服务的部署保持一致的风格。

先安装gunicorn,然后在项目根目录创建一个gunicorn.conf.py配置文件:

workers = 4 bind = "0.0.0.0:8000" timeout = 60 accesslog = "/var/log/food_project/gunicorn_access.log" errorlog = "/var/log/food_project/gunicorn_error.log"

启动:

gunicorn -c gunicorn.conf.py food_project.wsgi:application

8.2 nginx配置静态资源和反向代理

前后端分离的部署逻辑是:nginx监听80端口,Vue构建产物(dist目录)作为静态文件直接由nginx托管,同时把/api路径反向代理到django的8000端口,把/recommend路径代理到flask的5001端口。

nginx的关键配置:

server { listen 80; server_name your_domain.com; # Vue前端静态文件 root /var/www/food_frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } # Django后端接口 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Flask推荐服务 location /recommend { proxy_pass http://127.0.0.1:5001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # Django后台静态文件 location /static/ { alias /var/www/food_backend/static/; } # 媒体文件 location /media/ { alias /var/www/food_backend/media/; } }

location /里那个try_files非常关键,因为Vue路由是前端路由,直接访问/food/3这样的路径时,nginx服务器上并不存在这个文件,必须让它回退到index.html,然后交给前端路由处理。不写这个,线上刷新任何一个二级页面都会404。

8.3 Flask的独立部署和调用链路的可用性保障

flask推荐服务的部署方式和django类似,也用gunicorn。因为服务简单,2个worker就够了。生产环境里django调用flask的接口,我建议特别关注超时控制和降级逻辑,前面已经给了代码示例,这里再强调一下:推荐模块挂掉不应该影响到核心的菜品浏览功能。

8.4 数据库迁移和静态文件的坑

在服务器上跑迁移命令的时候,用这两句话是常规操作:

python manage.py makemigrations python manage.py migrate

但是有个很容易踩的坑:如果本地的数据库是SQLite,服务器上换成了MySQL,那本地生成的迁移文件在服务器上执行可能没问题,可如果你在本地加了依赖特定数据库的字段,比如JSONField在旧版本MySQL上可能不支持,迁移的时候就会报错。所以,生产环境数据库最好从第一天就用MySQL或者PostgreSQL,不要到了部署阶段再切换,临时切换的适配成本会让人崩溃。

静态文件在部署的时候需要执行python manage.py collectstatic,把所有app里的静态资源汇总到指定的STATIC_ROOT目录。settings.py里要配好:

STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles'

8.5 使用Systemd托管微服务的完整配置

为了能开机自启以及崩溃自动重启,用systemd管理django、flask、nginx这三个服务是比较标准的做法。新建一个/etc/systemd/system/food-backend.service:

[Unit] Description=Food Project Django Backend After=network.target [Service] User=www-data WorkingDirectory=/var/www/food_backend ExecStart=/var/www/food_backend/venv/bin/gunicorn -c gunicorn.conf.py food_project.wsgi:application Restart=always RestartSec=3 [Install] WantedBy=multi-user.target

然后执行:

systemctl daemon-reload systemctl enable food-backend systemctl start food-backend

flask推荐服务也照葫芦画瓢,换个服务和执行路径。这套管理方式非常可靠,服务挂了系统会在几秒内自动拉起来,不用专门写守护脚本。

9. 开发调试提速:Pycharm和Vue DevTools的使用心得

9.1 Pycharm里的Python路径和远程调试配置

Pycharm里配置Python解释器要特别注意项目的虚拟环境。在Settings里选择Project Interpreter,点Add Interpreter,选Existing,然后找到venv目录里的python.exe。这一步做对了,Pycharm的Terminal和Run按钮都会自动用虚拟环境启动。

调试django的时候,直接在Pycharm里配置一个Django Server运行配置,Debug模式可以直接在ORM查询处打断点,然后查看完整的SQL语句和变量状态,能省下非常多时间。DEBUG模式下config的断点命中完全无延迟,这一点比打log方便太多。

9.2 Vue DevTools检查前端组件和数据流

前端的调试,Vue DevTools是一件利器。装上浏览器插件之后,Vue 3项目可以在插件里直观看到组件树、props、state以及pinia store里的所有数据。我做列表页的时候,怀疑某个组件的状态更新逻辑有问题,利用DevTools直接修改store里的数据,页面实时变动,很快就定位到了问题,比反复加console.log效率高很多。

9.3 前后端并行开发的调试姿势

如果前后端要并行开发,Vite的代理转发支持直接配置。后端同学把自己写的接口部署到测试环境,前端把proxy的target指到测试环境地址,两边开发互不阻塞。我比较推荐Master分支和Dev分支分开管理,前端和后端各自在功能分支上开发,再统一合并测试。

10. 项目功能的扩展思路和优化空间

10.1 从单一接口到推荐服务升级的路径

现在推荐服务是比较基础的口味标签匹配,将来如果用户行为数据积累多了,可以升级成协同过滤算法,或者直接用Python的机器学习库训练排序模型。因为flask已经独立成一个服务了,算法替换这部分不会影响核心业务代码,这是一个很好的演进基础。

10.2 图片和静态资源上对象存储

当用户量变大,本地的/media目录会占用大量磁盘空间,而且备份困难。把图片上传迁移到云对象存储是必然的方向。django里写一个自定义storage类,或者用第三方插件,切换完之后ImageField完全不用改,业务代码无感知。

10.3 引入消息队列处理热门菜谱排名

如果后期要把“热门榜单”做实时的,那就有必要引入Redis缓存加队列了。用户每次点开菜谱详情,不直接写数据库,而是把事件推送到Redis里,异步任务定期把浏览量落库并更新排行榜。这样设计能明显降低数据库压力,还能做出实时TopN的效果。

不过作为一个美食分享项目,我个人的建议是不要一开始就上太重的架构。先把基础功能做扎实,跑起来,等用户量和数据量真上来了,再按需演进。过早引入复杂的中间件,只会让开发效率变慢,对早期项目来说是弊大于利。

我在这次项目里最大的体会是:一个项目中同时用django和flask,并不代表技术栈混乱,关键在于你愿不愿意事先想清楚“什么场景用哪个框架”。django管重逻辑的爽快和flask做轻量服务的灵活,搭档好了是能实实在在提升开发效率的。另外,不管是Python还是Vue,环境配置这块真的不要怕麻烦,一次性把虚拟环境、端口代理、状态管理这些地基打好,后面开发起来才会顺畅。希望这篇内容能帮你少走一点弯路,也欢迎你在实操中碰到问题的时候回来多交流。

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

STM32用MOS管替代继电器驱动12V气泵与电磁阀的完整实战

做嵌入式这段时间&#xff0c;我前前后后调过不少泵阀类的项目&#xff0c;其中最让我头疼的其实是驱动层那点事&#xff1a;一开始老老实实上继电器&#xff0c;结果电磁阀通断时“啪嗒啪嗒”的响声和触点火花总让人心里不踏实&#xff0c;气泵需要软启动时继电器又完全给不了…

作者头像 李华
网站建设 2026/9/29 15:44:50

东土交换机配置入门:串口连接、console认证与VLAN互通全解析

简介&#xff1a;本资源是一份面向工业网络工程师与现场调试人员的东土交换机实操配置指南&#xff0c;聚焦电力、轨道交通等对时延与可靠性要求较高的场景&#xff0c;系统解决设备部署、网络划分、安全加固及故障诊断等核心问题。文档以Word格式呈现&#xff0c;共1个DOC文件…

作者头像 李华
网站建设 2026/9/29 15:44:02

如何砍掉一半模型轮次:SoL-Pi Action Fusion编辑即运行新手教程

如何砍掉一半模型轮次&#xff1a;SoL-Pi Action Fusion编辑即运行新手教程 【免费下载链接】SoL-Pi SoL-Pi: Scaling Auto-Research Loops for Efficient Agent Harnesses 项目地址: https://gitcode.com/gh_mirrors/so/SoL-Pi SoL-Pi 是一款为 Pi 编码代理打造的效率扩…

作者头像 李华
网站建设 2026/9/29 15:43:56

BP神经网络光伏功率预测建模实战:从理论推导到避坑落地

简介&#xff1a;这是一份基于BP神经网络的光伏发电预测模型毕业论文文档&#xff0c;适合电气、能源、自动化等相关专业学生及科研人员参考&#xff0c;用于解决光伏发电量预测、电网运行稳定性等问题。文档结构完整&#xff0c;包含中英文摘要、关键词、目录、正文与结论&…

作者头像 李华
网站建设 2026/9/29 15:43:05

人脸表情识别模型zip解压到推理:格式识别与预处理全攻略

简介&#xff1a;面向人脸表情识别任务的模型文件压缩包&#xff0c;融合计算机视觉与深度学习技术&#xff0c;基于 PyTorch 框架提供 CNN、VGG、ResNet 三种经典网络的训练结果&#xff0c;其中 VGG 以多层小卷积核堆叠提取精细特征&#xff0c;ResNet 通过残差结构训练更深网…

作者头像 李华
网站建设 2026/9/29 15:42:30

实验室管理系统四件套交付实战:从源码部署到答辩避坑

简介&#xff1a;这套实验室管理系统是面向计算机专业毕业设计场景的完整Java项目&#xff0c;基于SpringBoot&#xff0b;MySQL构建&#xff0c;适合正在准备毕设的学生与需要项目实战的Java学习者&#xff0c;也可作为课程设计或期末大作业。系统分为用户端与管理员端&#x…

作者头像 李华