ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django+Vue茶叶商城全栈开发实战:从设计到部署完整复盘

Django+Vue茶叶商城全栈开发实战:从设计到部署完整复盘 做茶叶商城这个项目其实是被朋友的一句话推着走的。他说想搞个线上卖茶的铺子要能展示茶叶、能下单、能看订单最好以后还能搞活动。我寻思这不就是个典型的电商系统吗但真上手之后发现茶叶这个品类比想象中复杂——同样是绿茶产地不同、采摘时间不同、工艺不同价格能差出好几倍。这种“属性多、分类细、SKU看似简单实则复杂”的业务特征反而特别适合拿来做一个完整的前后端分离项目从数据库表设计到接口开发再到前端页面渲染每个环节都能踩到实实在在的坑。这篇文章就是从这个项目里整理出来的完整复盘从技术选型到数据库设计从后端接口到Vue前端实现再到最后的部署收尾每一步都记录了当时的思考和踩过的坑。适合正在学Python Web开发、准备做毕业设计或者想自己动手搞一个电商类项目的人参考。1. 项目定调网上茶叶商城的核心需求与技术选型1.1 茶叶商城的业务需求拆解开始写代码之前我习惯先把业务需求拆清楚。这个茶叶商城看起来只是个“卖货”的网站但真往细了想需要覆盖的模块一点儿也不少。用户侧的核心流程是注册登录 → 浏览商品 → 加入购物车 → 提交订单 → 模拟支付 → 查看订单状态。在此基础上还牵扯到商品分类筛选、茶叶详情展示产地、年份、工艺、储存方式等细碎信息、购物车数量修改、订单状态流转待支付、已支付、已发货、已完成这些环节。管理侧的需求更直接得能添加茶叶商品、维护库存、处理订单状态。如果从头写一套管理后台工作量不小但好在如果用Django自带的Admin后台稍微改改就能用这才是选型时的重要加分项。拆完需求之后整个项目的边界就清楚了。这是一个典型的“B2C零售系统”看着简单但涉及的东西很全用户体系、商品体系、交易体系、后台管理一套走完你对Web全栈的理解会提升一大截。1.2 Django与Flask的选型思考项目标题里同时出现了Django和Flask这其实是很多人在起步阶段的纠结。这两个框架我都有实际项目经验直接说说结论。选Django的理由在商城项目里非常明显自带ORM、Admin后台、认证系统、表单校验商城这种业务密集型项目能省下大量重复劳动自带的Admin后台改改配置就能给运营用否则你要为了“添加商品”这个功能单独写一整套管理页面Django的ORM在处理关联查询的时候非常顺手比如查某个用户的订单时连带查出订单项和商品信息select_related和prefetch_related用好了性能也不差Flask的优势在于灵活。它轻想怎么组织代码都行适合那种接口少、逻辑简单、需要高度定制化的项目。但商城这种模块多、关系复杂的项目用Flask就得自己搭结构、接ORM、写认证性价比不高。有一说一如果只是做一个非常轻量的展示型电商比如就几个页面、不涉及复杂订单流程Flask完全够用。但如果是想认真做一个能长期扩展的商城系统我不纠结直接推Django。1.3 为什么前端选择Vue后端定下来用Django前端我选了Vue 3。前后端分离是现在的主流做法前端的交互体验和页面渲染都更灵活后端只需要专心提供JSON接口就行。Vue的优势是渐进式项目简单的时候你可以只在一个页面里引入Vue做局部交互项目复杂了配合Vue Router做路由、Pinia或Vuex做状态管理一样能撑起完整的单页应用。这个项目里我们需要处理商品列表、商品详情、购物车状态、用户登录状态这些跨页面共享的数据用状态管理工具统一维护逻辑会清爽很多。选型这事儿没有绝对的对错关键是匹配项目规模和团队熟悉度。我见过有人用Flask加原生JavaScript也能把商城做完也有人用Django加Vue把项目做得特别重。这个茶叶商城项目我最终定下的技术栈是Django Django REST Framework Vue 3 Pinia MySQL开发IDE用PyCharm。2. 开发环境搭建与项目初始化2.1 PyCharm、Python与虚拟环境准备工欲善其事必先利其器。Python开发我用的是PyCharm社区版其实就够用了专业版支持Django模板提示和数据库工具体验更好但不用强求。Python版本这里提醒一句直接用3.10或3.11就行别用太老的3.7——Django新版本已经开始要求Python 3.10以上了。环境这块我踩过一个教训一定、一定、一定要用虚拟环境。刚开始学Python那会儿我图省事全局pip install结果项目一多依赖冲突得让人崩溃。这个茶叶商城项目依赖包括Django、Django REST Framework、django-cors-headers等各自有版本要求不用虚拟环境纯属给自己添堵。PyCharm新建项目的时候会默认帮你创建venv虚拟环境这个操作不要取消。# 在PyCharm终端或者命令行中确认虚拟环境已激活 python --version # 安装Django及项目依赖 pip install django djangorestframework django-cors-headers pillow # 检查安装版本 pip list | findstr Django # Windows pip list | grep Django # macOS/LinuxDjango版本我建议直接用最新的稳定版目前是4.x或5.xDRF则需要匹配对应版本。这里要注意一点django-cors-headers这个库是前后端分离开发的刚需不加它你Vue项目访问后端接口的时候会被浏览器的同源策略直接拦下。2.2 Django项目与Vue项目的目录规划我的习惯是前端和后端放在同一个总目录下但各自独立互不干扰。整体结构是这样tea_shop/ # 项目总目录 ├── backend/ # Django后端 │ ├── manage.py │ ├── tea_shop/ # Django项目配置目录 │ └── apps/ │ ├── users/ # 用户模块 │ ├── goods/ # 商品模块 │ ├── cart/ # 购物车模块 │ └── orders/ # 订单模块 └── frontend/ # Vue前端 ├── src/ │ ├── views/ # 页面组件 │ ├── components/ # 通用组件 │ ├── stores/ # Pinia状态 │ ├── router/ # 路由配置 │ ├── api/ # 接口封装 │ └── utils/ # 工具函数 ├── package.json └── vite.config.js创建Django项目的命令是# 在backend目录下 django-admin startproject tea_shop . python manage.py startapp users python manage.py startapp goods python manage.py startapp cart python manage.py startapp orders注意startproject命令后面那个点表示在当前目录生成项目文件不会再多套一层目录。这个细节看起来无关紧要但目录层级一旦多套一层后面写代码的时候导入路径就会很别扭。创建Vue项目我用的是Vite而不是Vue CLIVite启动速度快配置也简单# 在frontend目录下 npm create vitelatest . -- --template vue npm install npm install vue-router pinia axios这里直接在当前目录初始化Vite项目同样是为了避免目录嵌套过深。2.3 数据库选型与Django配置数据库我用的是MySQL因为MySQL是最通用的选择以后真要上线部署找运维资料也方便。开发环境我用的MySQL 8.0生产环境部署时同样用MySQL保持环境一致。在Django的settings.py里配置数据库连接# backend/tea_shop/settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: tea_shop, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }用MySQL还得安装一个连接驱动比如mysqlclient或者pymysql。在Windows上安装mysqlclient可能会遇到编译问题推荐直接用pymysql兼容性好安装也简单pip install pymysql然后在__init__.py里写入# backend/tea_shop/__init__.py import pymysql pymysql.install_as_MySQLdb()这一步是让Django以为你在用MySQLdb实际上是走了pymysql的兼容层。如果不做这一步Django连接MySQL的时候会直接报错“No module named MySQLdb”。还有一件事在settings.py的INSTALLED_APPS里把rest_framework和corsheaders加进去再把django-cors-headers的中间件注册到MIDDLEWARE里。这些都是后面开发接口的前置条件。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, corsheaders, users, goods, cart, orders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, django.middleware.security.SecurityMiddleware, ... ] CORS_ALLOW_ALL_ORIGINS True开发阶段CORS直接全放开就行上线之前再收紧不然前后端联调时跨域问题会浪费你大量时间。3. 数据库表设计与核心模块划分3.1 用户模块与Token认证设计网上商城系统里用户表是最基础也最关键的表。Django自带了一个User表但字段不一定够用我的做法是创建一个Profile模型和Django自带的User做一对一关联这样既能复用Django的认证逻辑又能扩展自己的字段。# backend/apps/users/models.py from django.contrib.auth.models import User from django.db import models class UserProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) phone models.CharField(max_length11, blankTrue, nullTrue) address models.CharField(max_length255, blankTrue, nullTrue) avatar models.ImageField(upload_toavatars/, blankTrue, nullTrue) created_at models.DateTimeField(auto_now_addTrue)注册登录的认证方案我选的是JWT先用的是djangorestframework-simplejwt这个库。用JWT的好处是前后端分离架构下不需要维护Session状态后端接口无状态化扩展和部署都方便。用户登录后拿到一个access_token后续每次请求在Authorization头里带上这个token后端通过认证中间件解析用户身份。# 安装 simplejwt pip install djangorestframework-simplejwt在settings.py里配置JWT认证from datetime import timedelta from rest_framework.settings import api_settings SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(hours2), REFRESH_TOKEN_LIFETIME: timedelta(days7), } REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), }JWT的过期时间我设的是2小时Refresh Token设7天。这个时间可以自己调但核心逻辑是access_token短一点安全refresh_token长一点方便用户自动续期。3.2 茶叶商品模型分类、属性与库存茶叶商品的设计是整个项目里最需要仔细琢磨的部分。很多新手做电商项目就直接建一个商品表字段写名称、价格、图片完事。但茶叶这个品类特殊光是一个商品名称就可能包含品牌、产地、原料、等级、年份、净含量、许可证号等一大堆信息。我设计的商品表核心字段大概这个样子# backend/apps/goods/models.py class Category(models.Model): name models.CharField(max_length50) # 绿茶、红茶、乌龙茶、白茶、黑茶 parent models.ForeignKey(self, nullTrue, blankTrue, related_namechildren, on_deletemodels.CASCADE) class Tea(models.Model): category models.ForeignKey(Category, related_nameteas, on_deletemodels.CASCADE) name models.CharField(max_length100) origin models.CharField(max_length50, blankTrue) # 产地 year models.IntegerField(blankTrue, nullTrue) # 年份 craft models.CharField(max_length50, blankTrue) # 工艺 price models.DecimalField(max_digits8, decimal_places2) # 单价 stock models.IntegerField(default0) # 库存 sales models.IntegerField(default0) # 销量 description models.TextField(blankTrue) # 详情描述 image models.ImageField(upload_toteas/, blankTrue) # 主图 is_active models.BooleanField(defaultTrue) # 是否上架 created_at models.DateTimeField(auto_now_addTrue)这里的Category用了自关联外键支持二级分类。虽然茶叶商城用一级分类可能就够但自关联的设计意味着以后如果要加“绿茶下的龙井、毛峰、碧螺春”这种子分类直接插数据就行不需要改表结构。产地、年份、工艺这几个字段虽然看起来不是必填项但实际是茶叶商品的核心卖点。建议在商品列表页就把产地和年份展示出来茶叶用户非常吃这一套。这属于业务细节不熟悉茶叶品类的人很容易忽略。库存字段必须用IntegerField而且每次下单更新库存的时候要注意并发问题这个后面在订单模块里细说。3.3 购物车、订单与订单项的表结构购物车表结构相对简单核心就是把“哪个用户买了哪个商品、买了几件”记录下来# backend/apps/cart/models.py class CartItem(models.Model): user models.ForeignKey(User, related_namecart_items, on_deletemodels.CASCADE) tea models.ForeignKey(goods.Tea, related_namecart_items, on_deletemodels.CASCADE) quantity models.PositiveIntegerField(default1) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, tea)unique_together的含义是同一个用户和同一种茶叶在购物车表里面只能有一条记录。这样设计的好处是用户重复点击“加入购物车”时我们只需要更新商品数量而不是新增记录。这个约束在数据库层面就保证了数据不会重复比在代码里判断靠谱得多。订单表要稍微拆开看。一个订单包含订单主表和订单项表主表记录订单总额、状态、收货信息等订单项表记录每个商品的下单数量、单价。这是电商系统的基础设计模式一定要拆开不然你无法处理“一个订单里有三种茶叶”的情况。# backend/apps/orders/models.py class Order(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 已支付), (shipped, 已发货), (completed, 已完成), (canceled, 已取消), ] user models.ForeignKey(User, related_nameorders, on_deletemodels.CASCADE) order_no models.CharField(max_length64, uniqueTrue) # 订单号 total_amount models.DecimalField(max_digits10, decimal_places2) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) address models.CharField(max_length255) # 收货地址 receiver models.CharField(max_length30) # 收货人 phone models.CharField(max_length11) # 联系电话 created_at models.DateTimeField(auto_now_addTrue) class OrderItem(models.Model): order models.ForeignKey(Order, related_nameitems, on_deletemodels.CASCADE) tea models.ForeignKey(goods.Tea, related_nameorder_items, on_deletemodels.PROTECT) tea_name models.CharField(max_length100) # 冗余商品名称防止商品修改后历史订单混乱 price models.DecimalField(max_digits8, decimal_places2) # 下单时价格快照 quantity models.PositiveIntegerField(default1) class Meta: unique_together (order, tea)OrderItem里我特地加了tea_name和price字段这两个不是冗余设计而是“快照”。为什么要做快照因为你卖的是茶叶商品价格和名称可能会调整但历史订单不能跟着变。用户一个月前买的龙井不能因为你今天把商品改名了他的历史订单就显示新名字。这笔账在开发时就要算清楚。4. 后端接口开发从Model到API的完整链路4.1 Django REST Framework的序列化器设计后端接口是前后端分离架构的桥梁。我用的Django REST Framework简称DRF提供了一套高度封装的API开发组件序列化器就是其中最关键的一环。序列化器做的事情就是把Django的Model对象转换成JSON返回给前端同时把前端传来的JSON校验后转换成Model存进数据库。商品列表的序列化器写法# backend/apps/goods/serializers.py from rest_framework import serializers from .models import Category, Tea class CategorySerializer(serializers.ModelSerializer): class Meta: model Category fields [id, name, parent] class TeaSerializer(serializers.ModelSerializer): category_name serializers.CharField(sourcecategory.name, read_onlyTrue) class Meta: model Tea fields [id, name, category, category_name, origin, year, craft, price, stock, sales, image, description]这里加了一个只读字段category_name因为前端商品列表一般只需要展示“这是什么分类”而不需要去联动查询分类表。这种在序列化器里直接取关联字段的方式比前端自己维护分类映射要省事得多。订单接口的序列化器复杂一些因为要支持嵌套class OrderItemSerializer(serializers.ModelSerializer): tea_name serializers.CharField(read_onlyTrue) class Meta: model OrderItem fields [id, tea, tea_name, price, quantity] class OrderSerializer(serializers.ModelSerializer): items OrderItemSerializer(manyTrue, read_onlyTrue) status_display serializers.CharField(sourceget_status_display, read_onlyTrue) class Meta: model Order fields [id, order_no, total_amount, status, status_display, receiver, phone, address, items, created_at]4.2 视图函数与API路由设置DRF提供了三种视图写法函数视图api_view、类视图APIView、视图集ViewSet。这个项目我混合使用了ViewSet和APIView。商品和分类这种以查询为主的模块用ViewSet很舒服因为它自动帮你实现了list、retrieve、create、update这些标准方法。# backend/apps/goods/views.py from rest_framework import viewsets from .models import Category, Tea from .serializers import CategorySerializer, TeaSerializer class CategoryViewSet(viewsets.ReadOnlyModelViewSet): queryset Category.objects.all() serializer_class CategorySerializer class TeaViewSet(viewsets.ReadOnlyModelViewSet): queryset Tea.objects.filter(is_activeTrue) serializer_class TeaSerializer def get_queryset(self): queryset super().get_queryset() category_id self.request.query_params.get(category) keyword self.request.query_params.get(keyword) if category_id: queryset queryset.filter(category_idcategory_id) if keyword: queryset queryset.filter(name__icontainskeyword) return querysetReadOnlyModelViewSet是只读视图集只有列表和详情两个接口商品展示正好用这个思路清晰也不容易出错。get_queryset方法里我做了分类筛选和关键词搜索这些都是商城系统的刚需功能。购物车和订单因为涉及写操作和用户绑定我用的是APIView代码更明确# backend/apps/cart/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated class CartListView(APIView): permission_classes [IsAuthenticated] def get(self, request): items CartItem.objects.filter(userrequest.user).select_related(tea) # 构造返回数据 ...路由配置在Django的urls.py里# backend/tea_shop/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from apps.goods.views import CategoryViewSet, TeaViewSet router DefaultRouter() router.register(categories, CategoryViewSet) router.register(teas, TeaViewSet) urlpatterns [ path(api/, include(router.urls)), path(api/auth/, include(djoser.urls)), # 或者自己写注册登录接口 path(api/cart/, include(apps.cart.urls)), path(api/orders/, include(apps.orders.urls)), ]4.3 用户注册登录与JWT鉴权流程用户注册登录这个环节我没有用Django自带的登录接口而是自己写了一套基于simplejwt的流程。注册接口的核心逻辑是前端把用户名和密码传过来后端创建User对象然后返回access_token和refresh_token。这样用户注册完就已经是登录状态不用再跳去登录页体验好很多。# backend/apps/users/views.py from django.contrib.auth.models import User from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken class RegisterView(APIView): def post(self, request): username request.data.get(username) password request.data.get(password) email request.data.get(email, ) if not username or not password: return Response({detail: 用户名和密码不能为空}, statusstatus.HTTP_400_BAD_REQUEST) if User.objects.filter(usernameusername).exists(): return Response({detail: 用户名已存在}, statusstatus.HTTP_400_BAD_REQUEST) user User.objects.create_user(usernameusername, passwordpassword, emailemail) refresh RefreshToken.for_user(user) return Response({ access: str(refresh.access_token), refresh: str(refresh), user: {id: user.id, username: user.username} }, statusstatus.HTTP_201_CREATED)对数据校验这里要特别说一下后端必须做校验不能光靠前端。前端校验只是为了用户体验后端校验才是安全保障。用户名是否重复、密码是否为空、密码长度是否达标这些必须后端判断一遍。恶意用户可以绕过前端直接调用你的接口你不可能指望他老老实实走页面流程。4.4 下单逻辑与库存并发处理下单是整个系统最核心的写操作。流程是这样的用户提交收货信息和购物车ID列表后端验证用户身份和收货信息计算订单总金额扣减库存生成订单和订单项清空购物车第4步扣库存这块是最容易出问题的。假设只有一个用户下单事情很简单库存减一就行。但如果是秒杀场景几百上千人同时下单每个人都读到库存大于0然后都执行减库存最后库存就变成负数了。Django的ORM在并发场景下读取和写入不是原子的。简单写法是tea Tea.objects.get(idtea_id) if tea.stock quantity: tea.stock - quantity tea.save() # 这样写有并发风险更稳妥的做法是使用Django的F表达式做原子更新from django.db.models import F updated Tea.objects.filter(idtea_id, stock__gtequantity).update(stockF(stock) - quantity) if updated 0: return Response({detail: 库存不足}, statusstatus.HTTP_400_BAD_REQUEST)这段查询的作用是只有当当前库存大于等于购买数量时才执行减库存的操作。数据库层面的条件更新保证了原子性在并发场景下不会出现超卖。这个技巧是电商开发里教科书级别的坑不管项目大小都应该按这个思路来写。下单还需要生成订单号。我的方案是时间戳加用户ID再加随机数import time, random def generate_order_no(user_id): ts time.strftime(%Y%m%d%H%M%S) rand random.randint(1000, 9999) return f{ts}{user_id}{rand}订单号的格式没有统一标准但核心要求是唯一、可读、不暴露太多业务信息。我见过有些项目直接用UUID其实也可以就是订单号太长客户售后报订单号的时候报起来很痛苦。5. Vue前端开发商品展示与购物车交互5.1 Vue项目结构与路由配置前端的开发我用了Vue 3的Composition API也就是script setup的写法这种写法相比Vue 2的Options API更简洁代码组织也更灵活。在Vite创建的Vue项目里我先把路由配好// frontend/src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: home, component: () import(../views/HomeView.vue) }, { path: /goods/:id, name: goods-detail, component: () import(../views/GoodsDetailView.vue) }, { path: /cart, name: cart, component: () import(../views/CartView.vue) }, { path: /orders, name: orders, component: () import(../views/OrderListView.vue) }, { path: /login, name: login, component: () import(../views/LoginView.vue) }, { path: /register, name: register, component: () import(../views/RegisterView.vue) }, ] const router createRouter({ history: createWebHistory(), routes, }) // 全局路由守卫需要登录的页面做跳转 router.beforeEach((to) { const token localStorage.getItem(access_token) if (to.name cart || to.name orders) { if (!token) return { name: login } } }) export default router我用了Vue Router的懒加载通过import()函数动态引入组件这样首屏加载不需要一次性把所有页面代码都下载下来对性能有好处。路由守卫里判断了需要登录的页面没有token就强制跳登录页这个逻辑是商城系统的标配。5.2 商品列表与详情页的实现商品列表页是商城对外的门面用户体验好不好第一印象非常重要。我采用的是“左侧分类侧边栏 右侧商品卡片网格”的经典布局。左侧从后端接口拉取分类列表点击不同的分类会携带参数重新请求商品接口。!-- frontend/src/views/HomeView.vue -- script setup import { ref, onMounted, watch } from vue import { getCategories, getTeas } from ../api/goods import GoodsCard from ../components/GoodsCard.vue import { useRoute } from vue-router const route useRoute() const categories ref([]) const teas ref([]) const loading ref(false) async function loadGoods() { loading.value true const params {} if (route.query.category) params.category route.query.category if (route.query.keyword) params.keyword route.query.keyword teas.value await getTeas(params) loading.value false } onMounted(() { getCategories().then(res categories.value res) loadGoods() }) watch(() route.query, loadGoods) /script商品卡片组件我用了一个比较讨巧的做法把价格用醒目的颜色和大号字体展示出来把“产地”和“年份”这两个关键字放在价格下面。这两个字段是茶叶用户在浏览时最关心的核心信息比放一段又长又绕的商品描述有用得多。详情页更直接顶部是商品大图下面跟着价格、库存、产地、年份、工艺等参数表再往下是商品描述。我一般会在这个页面加一个“加入购物车”按钮和一个“立即购买”按钮两个按钮对应同一个接口只是在加入购物车成功后跳转到购物车页面立即购买则直接跳提交订单页。5.3 Pinia状态管理与购物车交互逻辑购物车的数据属于跨页面共享状态。用户可能在下单页操作也可能在商品详情页操作购物车里装了什么商品在多个组件里都要用。这种情况用Pinia做全局状态管理再合适不过。// frontend/src/stores/cart.js import { defineStore } from pinia import { getCart, addCart, updateCartItem, removeCartItem } from ../api/cart export const useCartStore defineStore(cart, { state: () ({ items: [], totalPrice: 0, count: 0, }), getters: { // 计算总价 calculatedTotal: (state) { return state.items.reduce((sum, item) sum item.price * item.quantity, 0) } }, actions: { async fetchCart() { this.items await getCart() this.count this.items.length }, async addItem(teaId, quantity) { await addCart(teaId, quantity) await this.fetchCart() }, async updateQuantity(id, quantity) { await updateCartItem(id, quantity) await this.fetchCart() }, async removeItem(id) { await removeCartItem(id) await this.fetchCart() } } })购物车页面的核心交互有两个修改数量和删除。修改数量时前端会调用接口更新后端数据同时重新拉取购物车列表。这里我踩过一个性能上的坑不要每次点加减按钮都全量刷新购物车数据。比如用户连续把某个商品数量从1加到5每加一次就发一次请求网络开销大体验也不好。我的做法是本地先改数量点击加减后先更新UI等用户停止操作或离开页面时再统一提交。当然这是优化项项目初期直接每次都调用接口也问题不大。5.4 Axios封装与前后端联调问题前后端联调是开发中最容易卡住的环节问题通常集中在两个地方跨域和Token传递。Axios封装的核心思路是拦截器。请求拦截器负责在每次请求的header里带上token响应拦截器负责统一处理错误状态码遇到401就跳登录页。// frontend/src/utils/request.js import axios from axios import router from ../router const request axios.create({ baseURL: http://127.0.0.1:8000/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 error.response.status 401) { localStorage.removeItem(access_token) router.push({ name: login }) } return Promise.reject(error) } ) export default request这里有个开发阶段很常见的坑就是图片路径问题。Django后端的商品图片是通过ImageField存到本地的数据库里存的是一个相对路径比如/media/teas/2024/longjing.jpg。前端Vue项目拿着这个路径直接放到img标签里会拼成http://localhost:5173/media/...肯定访问不到。解决方法是给前后端约定一个静态资源访问规则Vite的简单配置// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: http://127.0.0.1:8000, /media: http://127.0.0.1:8000 } } })通过Vite的代理配置前端请求/media/xxx时会把请求转发到后端的8000端口这样图片就能正常显示了。同理/api路径也通过代理转发这样在开发阶段前端代码里就不需要写完整的后端地址统一以/api开头就行。6. 图片上传、安全加固与收尾部署6.1 Django图片上传与Media文件配置商城系统的图片上传是一个绕不开的功能。商品主图、用户头像都得走上传逻辑。Django的ImageField默认把图片存到本地磁盘这个机制对中小型项目完全够用。在settings.py里配置# 图片上传目录 MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)然后在项目的urls.py里加上开发阶段的媒体文件访问路由from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 其他路由 ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这里只适用于DEBUG模式上线后应该由Nginx来提供静态文件和媒体文件的访问不能让Django处理这些静态资源性能撑不住。6.2 商城系统的安全注意事项安全是电商系统开发中最不能糊弄的部分但也是初学者最容易忽略的部分。我梳理一下咱这种项目里必须注意的几个点。密码必须哈希存储。Django的User模型默认使用PBKDF2算法对密码做哈希create_user方法本身就会处理你千万不要手动把明文密码存进数据库。验证密码的时候用user.check_password(raw_password)不要自己对比。JWT令牌的分发和管理。前后端分离架构下token是用户身份的唯一凭证泄露了就等于账号拱手让人。前端不要把token放在URL参数里必须放在Authorization头里。开发阶段可以在localStorage存token但上线后推荐改成HttpOnly Cookie存储可以有效降低XSS攻击导致token泄露的风险。商品接口的越权问题。比如订单接口用户只能查看自己的订单不能看到别人的订单。用DRF的get_queryset方法做权限控制是标准做法# backend/apps/orders/views.py class OrderListView(APIView): permission_classes [IsAuthenticated] def get(self, request): orders Order.objects.filter(userrequest.user).prefetch_related(items) serializer OrderSerializer(orders, manyTrue) return Response(serializer.data)这个过滤条件至关重要。有些新手会直接写Order.objects.all()然后让前端自己筛选这在商城系统里属于严重的安全漏洞。6.3 部署方案的简化选择部署这块我不想讲得太复杂因为很多人的项目只是课程设计或练手项目。如果只是课程设计交作业本地跑起来就算完成。但如果真要公网访问我的经验是先别急着上Kubernetes、Docker那一套先把最简单的方式跑通。最简单的方案是一台云服务器Nginx Gunicorn Django Vue打包静态文件。大概流程是Django后端用Gunicorn跑起来监听8000端口Vue项目执行npm run build生成dist目录Nginx把dist目录作为静态文件服务的根目录访问/时返回前端页面Nginx把/api开头的请求反向代理到Gunicorn把/media开头的请求代理到Django或直接指向media目录Nginx配置的大致样子server { listen 80; server_name your_domain.com; # 前端静态文件 root /path/to/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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /media { alias /path/to/backend/media; } # Vue history 模式刷新支持 location / { try_files $uri $uri/ /index.html; } }Nginx配置里最后那个try_files特别关键。Vue用了history模式的路由后页面路径不是真实文件路径。用户刷新/goods/3这个地址时Nginx发现没有这个文件就会返回404。try_files的作用就是找不到文件时回退到index.html由Vue Router接管路由。这一句不加上线的商城一刷新就白屏我当年在这个问题上栽过跟头。6.4 Django自带Admin后台的使用说了半天前后端分离其实后台管理这个事儿有个更省力的方案直接用Django的Admin后台。Django Admin几乎是白送的报表后台。你只需要把Model注册进去就能在线增删改查茶叶商品、管理订单状态、查看用户列表。对于商城运营来说这个后台比自研管理页面好用太多了。# backend/apps/goods/admin.py from django.contrib import admin from .models import Category, Tea admin.register(Tea) class TeaAdmin(admin.ModelAdmin): list_display [name, category, price, stock, sales, is_active] list_filter [category, is_active, year] search_fields [name, origin] list_editable [price, stock] # 列表页直接改价格和库存Admin后台的list_display控制列表显示哪些字段list_filter生成筛选器search_fields支持搜索。list_editable让运营人员不用进入详情页就能直接改价格和库存效率非常高。我自己做商城项目后台管理这块的策略是Admin管数据自研接口管用户端。这个组合既省力又清晰后台给内部用接口给前端用互不干扰。7. 常见问题与排查技巧实录7.1 跨域问题的完整排查流程前后端分离项目遇到的第一个拦路虎绝对是跨域。浏览器地址是http://localhost:5173接口地址是http://127.0.0.1:8000不同端口就算跨域了。我的排查流程是这样的先看报错信息浏览器开发者工具里的Console和Network面板会明确告诉你哪个请求被CORS拦截确认后端是否安装了django-cors-headers并且加到了INSTALLED_APPS和MIDDLEWARE里确认中间件的位置CorsMiddleware必须在CommonMiddleware之前开发阶段直接设置CORS_ALLOW_ALL_ORIGINS True先把功能跑通上线前把全放开改成白名单模式只允许你的前端域名访问中间件的顺序这个问题很隐蔽。Django中间件按列表从上到下执行如果你把CorsMiddleware放在最后面它的配置可能不会被先执行的其他中间件所感知。曾经我花了一整晚排查跨域问题最后发现只是中间件顺序不对。7.2 Django ORM查询中的高频坑点ORM查询看起来简单实际用起来有几个高频坑。第一个坑是忘记写.all()或者.first()。很多人写Tea.objects.filter(is_activeTrue)拿到的是一个QuerySet然后直接序列化传给前端会报Object of type QuerySet is not JSON serializable。但更隐蔽的情况是你拿这个QuerySet去模板渲染没事但用DRF时就必须先经过序列化器处理。第二个坑是查询今天创建的订单这类时间范围查询。用created_at__date做日期过滤是可行的但大数据量下性能不好因为__date会阻止数据库索引生效。正确写法是用rangefrom django.utils import timezone today_start timezone.now().replace(hour0, minute0, second0, microsecond0) today_orders Order.objects.filter(created_at__gtetoday_start)第三个坑是N1查询问题。比如查询订单列表时每条订单都要查它关联的订单项如果不加prefetch_relatedORM会对每条订单分别发一条查询50条订单就是51条SQL语句。加上prefetch_related(items)之后查询次数会降到2次。商城列表页这种场景性能差距非常明显。7.3 Vue中的典型问题与解决办法Vue开发遇到的问题更多是细节层面的。我挑了三个典型问题都是实战中几乎必遇的。路由跳转但页面不更新。商品列表页在按分类筛选时虽然路由变化了但组件实例没有重建onMounted不会再次触发。解决办法就是用watch监听路由对象的变化在回调里重新请求数据。Vue响应式丢失。直接给ref的value重新赋值数据变了但页面不刷新。这个问题的根源通常是你把响应式对象传给了一个非响应式的地方然后在里面修改了它的值。比如把reactive对象传给一个普通函数去修改属性修改操作不会触发响应。用ref和reactive时一定要弄清楚哪些数据是响应式的新增属性时要用$set或直接替换整个对象。开发环境的模块缓存。Vite开发模式有热更新但有时候你改了代码页面没反应这个问题大概率是因为模块依赖图缓存。重启开发服务器基本能解决。7.4 排查问题的通用方法论最后分享一个我调试项目时屡试不爽的方法论。很多新人遇到Bug慌得不行我的建议是别急着改代码先确认问题到底出在哪一层。前后端分离项目的故障链路是前端页面 → 接口请求 → 后端逻辑 → 数据库。问题可能出在任意一环。我的排查顺序是打开浏览器开发者工具的Network面板看接口到底返回了什么如果接口返回500直接去Django后端看日志找到Traceback如果是数据不对用Django shell手动执行ORM查询看看是不是数据本身有问题如果前端页面渲染不对在Vue组件里打印数据看传给组件的数据结构是否和后端期望的一致这个方法讲起来简单但能帮你节省大量无效调试的时间。技术栈再复杂核心思路永远是“先确认问题在哪个环节再针对那个环节做深入排查”。8. 写在最后的一些体会茶叶商城这个项目从设计到实现前前后后花了两周左右的时间中间踩了不少坑但也把前后端分离开发的全流程走了一遍。我个人做这类全栈项目的体会是先规划后动手真的很重要再把一个完整流程从头到尾走通比浏览十篇教程都有用。如果后面想继续扩展有几个方向值得尝试接入真实支付渠道和退款流程这个对你的代码能力是个不小的考验加上用户评论和评分功能让茶叶的口碑能积累下来做后台数据统计看板把销量、热门商品这些数据可视化展示出来。这几个方向各自都很有内容也能让这个茶叶商城项目在简历上更有含金量。最后提醒一句做完项目一定要及时写总结记录过几个月回看你会发现那些踩坑经验才是你真正的收获。
返回列表