资讯动态

Django+Flask+Vue全栈实战:茶叶商城开发复盘与避坑指南

发布时间:2026/10/8 10:02:14 来源:尧图企业网站定制
做了两套完整版本、前后台加起来差不多四千行代码最后上线跑通的那一刻我才真正理解了“网上茶叶商城”这类垂直电商项目为什么值得认真做一遍。茶叶不是标准品同样是龙井产地、年份、等级、采摘时间不同价格能差出几十倍这让商品模型、订单逻辑、搜索筛选都比普通的“卖书卖文具”案例复杂一个档次。这篇文章把我从技术选型到数据库设计、从 Django/Flask 双后端实现到 Vue 前端联调的全过程复盘一遍包括我在 PyCharm 里的环境配置和一些网上教程不会写的坑给正在做毕设、课程设计或者想练手全栈的同学一条可以直接抄的路线。1. 项目定位为什么偏偏是“茶叶商城”1.1 垂直电商项目的核心价值很多人做电商练手项目喜欢选“通用商城”商品就放个名字和价格页面做出来千篇一律。我之所以选茶叶品类是因为这个场景把电商系统里最麻烦的几个问题全带出来了多级分类、多重属性、库存变动、订单状态流转、搜索排序。茶叶有“六大茶类”的天然层级——绿茶、红茶、乌龙茶、白茶、黑茶、黄茶每个大类下面又有具体品种比如绿茶下面分龙井、碧螺春、毛峰这种分类结构做不好后面商品管理就是一团乱麻。另外茶叶的定价逻辑也很特别。它不像数码产品有个官方建议零售价同样一饼普洱山头不同、年份不同、仓储条件不同价格可能从几十到几万。所以我设计商品表的时候保留了price销售价和market_price市场参考价两个字段方便页面展示“促销价对比原价”的效果同时留出origin产地、year年份、level等级这些维度字段为后面的筛选和搜索埋好伏笔。1.2 Django 和 Flask 怎么选我在开发过程中其实把 Django 和 Flask 两套都完整做了一遍不是闲得慌而是这两套方案代表了完全不同的两种开发思路各有各的适用场景。对比维度DjangoFlask项目结构自带 app 划分、ORM、Admin、迁移工具轻量灵活结构自由上手曲线概念多初次接触会有点懵简单直接适合快速出接口ORM 能力强QuerySet链式查询非常顺手需要自己装 SQLAlchemy 或原生 SQL后台管理自带 Admin数据维护几乎零成本需要自己写或用第三方扩展适合场景后台逻辑复杂、需要快速搭建完整系统轻量接口、原型验证、前后端分离如果目标是做一个功能完整的商城我推荐 Django 作为主力后端因为用户、订单、商品、评论这些模块都是天然的分区Django 的 app 机制简直是为这种业务量身定做的。但如果你只是想验证某个页面效果、做个简单的数据可视化接口Flask 更合适。我下面会把两套方案的核心代码都给出来你按自己的需求挑一套跟着做就行。1.3 前端用 Vue 的理由前端选 Vue 而不是 React主要是两个原因一是 Vue 的模板语法和 Django/Flask 的模板理念一脉相承从后端转过来的同学上手极快二是 Vue 的生态足够齐全Element Plus 之类的 UI 组件库把表格、弹窗、分页、表单校验全打包好了商城的后台管理页面几乎是在“搭积木”。Vue 3 搭配 Vite 做开发服务器热更新速度快到几乎无感改一行代码浏览器立刻刷新这种体验对调试商品列表和购物车交互特别重要。后面我会讲 Vue 环境配置的具体操作。2. 数据库设计一张表一个坑2.1 用户、商品、购物车、订单四张核心表商城系统的数据库设计是决定后期开发效率的关键。我第一版图省事把订单和订单详情揉成一张表结果做支付流程时发现根本没法处理“一个订单包含多个商品”的情况只能推倒重来。正确的做法是把订单拆成订单主表order和订单明细表order_item一对多的关系一个订单对应多个商品条目。用户表除了基本的用户名字段我还加了phone、address两个字段方便后面做收货地址管理和订单配送。商品表专门留了一个is_active布尔字段作用是下架商品而不是直接删除——这是从真实运营角度考虑的商品下架后历史订单仍然需要展示信息物理删除会牵动一堆外键关联。用户表(user)id, username, password_hash, phone, email, created_at 商品表(tea)id, name, category_id, origin, year, level, price, market_price, stock, image, description, is_active, created_at 购物车表(cart)id, user_id, tea_id, quantity, checked 订单表(order)id, order_no, user_id, total_amount, status, address, create_time 订单明细表(order_item)id, order_id, tea_id, tea_name_snapshot, price, quantity商品表里我特意加了tea_name_snapshot这个“快照字段”。这不是标准设计但非常实用——用户下单后如果商家改了商品名称或价格历史订单里仍然保留下单那一刻的信息不会出现“订单里显示的和当时买的不是一回事”的纠纷。2.2 茶叶商品模型的特殊处理茶叶商品的多维属性如果不做专门设计后期开发搜索功能时会非常痛苦。我在category表里通过自关联实现了无限级分类父分类下挂子分类代码实现如下# Django models.py from django.db import models class Category(models.Model): name models.CharField(分类名称, max_length50) parent models.ForeignKey( self, on_deletemodels.CASCADE, nullTrue, blankTrue, related_namechildren, verbose_name父分类 ) class Meta: verbose_name 商品分类 verbose_name_plural verbose_name def __str__(self): return self.name商品表通过外键关联分类再加上产地、年份、等级三个平行维度字段这样前端筛选栏就可以做成“分类 产地 年份”三联动的组合筛选。数据库里的索引也很重要我在category_id、origin、year这三个字段上加了组合索引因为商城首页最频繁的查询就是这类多条件筛选没有索引的话数据量一上来接口会肉眼可见地变慢。3. 后端实现Django 主力 Flask 对照3.1 Django 项目搭建全流程我使用 PyCharm 的 Professional 版新建 Django 项目建完之后会自动生成manage.py、配置目录和一个初始应用。网上的 Django 教程很多都是老版本安装新版本后有几个地方要注意。我用的命令顺序是创建虚拟环境、安装依赖、创建项目、创建 app、配置数据库、迁移、启动服务。# 创建并激活虚拟环境 python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # macOS/Linux # 安装核心依赖 pip install django djangorestframework django-cors-headers pillow # 创建项目和 app django-admin startproject tea_shop . python manage.py startapp goods python manage.py startapp user python manage.py startapp order这里特别说明一下我用startapp按业务模块拆分了三个 app而不是把全部代码塞在同一个 app 里。Django 官方推荐这种“一个 app 管一件事”的组织方式goods管商品和分类user管账户和收货地址order管购物车和订单。后面写代码的时候你会发现模型、视图、路由都归类得清清楚楚找代码比翻文件快得多。3.2 Django 的查询、删除对象操作后端接口里最常用的操作就是增删改查。我用 Django REST Framework 来写 API一个视图集把商品列表、详情、创建、更新、删除全包了代码量非常少# goods/views.py from rest_framework import viewsets, filters from django_filters.rest_framework import DjangoFilterBackend from .models import Tea from .serializers import TeaSerializer class TeaViewSet(viewsets.ModelViewSet): queryset Tea.objects.filter(is_activeTrue).select_related(category) serializer_class TeaSerializer filter_backends [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields [category_id, origin, year] search_fields [name, description, origin] ordering_fields [price, created_at]Django 查询对象是真的写起来很爽select_related(category)一键解决外键连表查询的 N1 问题filterset_fields直接让前端传?category_id1origin西湖这样的参数做筛选搜索和排序也自动生成接口参数。关于删除对象我强烈建议不要物理删除商品。Django 虽然提供了delete()方法但我所有商品删除操作都改成把is_active置为 False查询时统一加过滤条件这样历史订单和购物车里还留存着的商品就不会出现外键指向空数据的尴尬。只有当分类下没有任何商品且没有子分类时才允许物理删除这属于纯粹的数据维护操作。3.3 Flask 版本的等价实现如果你偏好 Flask这套系统的代码量会更小但很多东西要自己搭。我用 Flask 写了一个简化版核心路由代码如下# app.pyFlask 方案 from flask import Flask, jsonify, request from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///tea_shop.db db SQLAlchemy(app) CORS(app) # 商品模型 class Tea(db.Model): __tablename__ tea id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(100), nullableFalse) price db.Column(db.Numeric(10, 2), nullableFalse) origin db.Column(db.String(100)) stock db.Column(db.Integer, default0) def to_dict(self): return { id: self.id, name: self.name, price: float(self.price), origin: self.origin, stock: self.stock } app.route(/api/teas, methods[GET]) def get_teas(): category_id request.args.get(category_id) query Tea.query if category_id: query query.filter_by(category_idcategory_id) teas query.all() return jsonify([tea.to_dict() for tea in teas]) app.route(/api/teas/int:tea_id, methods[DELETE]) def delete_tea(tea_id): tea db.session.get(Tea, tea_id) if tea is None: return jsonify({error: 商品不存在}), 404 db.session.delete(tea) db.session.commit() return jsonify({message: 删除成功})Flask 和 FastAPI 比较的话如果你要做高并发异步接口FastAPI 有天然的async优势和自动生成的 API 文档但商城这种以 CRUD 为主的业务Flask 的简单直接反而更稳。Flask 写完后不用担心框架层面的复杂概念重心全放在业务本身。3.4 购物车与订单状态流转购物车接口看似简单其实有个隐藏问题用户反复把同一件商品加入购物车数据库里不应该出现两条记录而应该把数量叠加。所以我加了一个update_or_create逻辑# Django 购物车接口核心代码 from django.shortcuts import get_object_or_404 from .models import Cart, Tea def add_to_cart(request): user request.user tea_id request.data.get(tea_id) quantity int(request.data.get(quantity, 1)) tea get_object_or_404(Tea, pktea_id, is_activeTrue) cart_item, created Cart.objects.get_or_create( useruser, teatea, defaults{quantity: quantity, checked: True} ) if not created: cart_item.quantity quantity cart_item.save() return JsonResponse({message: 已加入购物车, quantity: cart_item.quantity})订单状态流转我用了固定的字符串常量待支付、待发货、待收货、已完成、已取消。状态机的好处是前端展示进度条时逻辑完全统一后端也只需要一个字段控制不会出现状态之间无法跳转的问题。4. Vue 前端从零开始搭商城页面4.1 Vue 环境安装与项目初始化如果你之前没用过 Vue最省事的方式是直接用 Vite 脚手架不需要手动去配 webpack。我在全新环境下的安装步骤是这样# 安装 Node.js 后确认版本 node -v npm -v # 使用 Vite 创建 Vue 3 项目 npm create vitelatest tea_shop_frontend -- --template vue # 进入项目并安装依赖 cd tea_shop_frontend npm install # 安装路由、状态管理和 HTTP 库 npm install vue-router4 pinia axios # 安装 Element Plus 组件库 npm install element-plus # 启动开发服务器 npm run devVite 的启动速度真的比旧版 Vue CLI 快很多第一次跑起来基本就是秒开。npm install的时候如果卡住大概率是网络问题执行下述步骤将 registry 切换为国内镜像后重新安装即可npm config set registry https://registry.npmmirror.com npm install4.2 页面结构和组件拆分商城前端我会按这样的目录结构组织src/ ├── api/ # 接口请求封装 │ ├── tea.js # 商品相关接口 │ ├── cart.js # 购物车相关接口 │ └── order.js # 订单相关接口 ├── views/ # 页面级组件 │ ├── Home.vue # 首页 │ ├── ProductList.vue # 商品列表/搜索页 │ ├── ProductDetail.vue # 商品详情 │ ├── Cart.vue # 购物车 │ └── OrderConfirm.vue # 订单确认 ├── components/ # 可复用组件 │ ├── Header.vue │ ├── ProductCard.vue │ └── Pagination.vue └── router/index.js # 路由配置页面路由用 Vue Router 管理一个典型的路由配置长这样// router/index.js import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /, component: () import(/views/Home.vue) }, { path: /products, component: () import(/views/ProductList.vue) }, { path: /products/:id, component: () import(/views/ProductDetail.vue) }, { path: /cart, component: () import(/views/Cart.vue) } ] }) export default router商品列表页是整个前端交互最复杂的页面筛选条件多、分页逻辑要考虑、还要适配不同屏幕尺寸。我用了 Element Plus 的el-checkbox-group做多选筛选配合el-select做下拉选择整体交互比较顺。核心代码如下!-- components/ProductCard.vue -- template el-card classproduct-card shadowhover img :srctea.image_url classtea-image :alttea.name / div classtea-info h3{{ tea.name }}/h3 p classtea-meta{{ tea.origin }} · {{ tea.year || 新茶 }}年/p p classtea-price span classnow-price{{ tea.price }}/span span v-iftea.market_price classold-price{{ tea.market_price }}/span /p el-button typedanger clickaddToCart加入购物车/el-button /div /el-card /template script setup import { useRouter } from vue-router import { ElMessage } from element-plus const props defineProps({ tea: { type: Object, required: true } }) const router useRouter() async function addToCart() { // 调用购物车接口 const res await fetch(/api/cart/add, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ tea_id: props.tea.id, quantity: 1 }) }) if (res.ok) { ElMessage.success(已加入购物车) } } /script4.3 前后端联调跨域与代理配置开发阶段最烦的问题就是跨域。后端跑在 localhost:8000前端跑在 localhost:5173如果直接发请求肯定被浏览器拦截。解决办法有三种后端开启 CORS、前端配代理、或者部署到同域。开发阶段我推荐在前端 Vite 配置里做代理简单又不会污染后端代码// 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://localhost:8000, changeOrigin: true } } } })这样前端代码里请求/api/teas开发服务器会自动转发到 Django 的 8000 端口浏览器感知不到跨域问题就解决了。如果后端也用 Flask同样在 Flask 里装一个flask-cors就可以直接放行。5. 常见问题与排查技巧实录我开发过程中踩了不少坑整理成一个速查表优先级从高到低排列问题现象根本原因解决办法npm install一直转圈默认 npm 源在国外网络不稳更换为国内镜像源Vue 页面空白控制台报错路由使用了createWebHistory但部署环境没有做 history 回退开发用 history部署改createWebHashHistory或配 Nginxtry_filesDjango 接口数据前端拿不到未配置 CORS 或代理开发用 Vite proxy生产用 Nginx 反向代理图片上传后显示不出来MEDIA_URL 和生产静态文件配置不一致检查settings.py里的MEDIA_URL、MEDIA_ROOT和 Nginx 静态资源映射订单结算时库存超卖用户同时下单时库存字段存在竞态在扣库存的数据库操作上加F()表达式或事务锁Django 查询慢没用select_related外键查询是逐条查数据库查询时加select_related(category)商品删除后订单页面报错商品物理删除历史订单外键悬空保留商品记录改用is_active下架或删除时做快照其实最坑的是 Python 环境的问题。我见过不少同学在 PyCharm 里新建项目结果用的还是全局 Python 解释器装的包全混在一起版本冲突到崩溃。用 App 开发的第一步就是在 PyCharm 里先建虚拟环境路径一般在 PyCharm 右下角选择 “Virtualenv”它会自动帮你隔离环境然后pip install都在这个环境里操作干净又卫生。关于 PyCharm 的配置我推荐几个让效率翻倍的操作设置自动换行、配置模板头、关闭代码折叠提示、装上官方中文插件如果写 Vue 也想有智能提示可以装 Vue Language Features 相关的插件。PyCharm 跑本地开发服务的时候直接把 Django 配置为虚拟环境里的python在 Run/Debug Configurations 里加一行manage.py runserver就行不用每次开终端。6. 上线部署与后续扩展6.1 基本的部署思路本地开发和线上部署的环境差异是新手最容易忽视的。我在部署时大致分了这几步后端用 Gunicorn 跑 DjangoNginx 做反向代理配 HTTPS。前端npm run build生成静态文件交给 Nginx 托管。数据库本地用 SQLite 方便调试线上换 MySQL 或 PostgreSQL。如果只是交作业或者自己测试最简单的方式是把前端构建后的dist目录放在 Nginx 里把/api路径代理到 Gunicorn 监听端口server { listen 80; server_name your_domain.com; root /var/www/tea_shop_frontend/dist; index index.html; location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }把 Vue 打包后放进 Django 托管也是一个常见做法把dist拷到 Django 的static目录再在urls.py加一个路由指向index.html。不过前后端分离的项目我更推荐 Nginx 托管前端、API 单独走代理这样后面前端和后端可以各自独立扩展。6.2 还能怎么玩出花这套系统做完基础版后扩展方向其实非常多。比如做价格走势图表用 ECharts 展示某款茶叶一年内的价格波动——其实和“农产品价格数据可视化”这类项目的思路完全一样比如做推荐系统根据用户的浏览和购买记录用简单的协同过滤推荐同产地或同品类的茶叶再比如做订单报表用 Django admin 内置功能加上简单的图表展示销售统计。我自己的体会是项目做没做完最后看的不是功能列表有多长而是你有没有真正常握了从设计到落地的完整链路。茶叶商城这套项目把用户系统、商品系统、购物车订单系统、后台管理系统全串起来前端还涉及组件通信、状态管理、路由守卫这些 Vue 的核心内容一套做完对 Python 后端和 Vue 前端的理解绝对上了一个台阶。如果看完这篇你准备开搞我的建议就一句先用 Django Vue 把主流程跑通再考虑 Flask 精简版或者加花哨功能稳扎稳打出成果。

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

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

免费获取报价 →
↑