Python Django/Flask + Vue全栈开发家居商城系统实战解析

发布时间:2026/10/7 12:18:07
Python Django/Flask + Vue全栈开发家居商城系统实战解析 1. 先把项目讲清楚这个家居商城到底在做什么“django-flask基于python网上家居商城系统pycharm -Vue”这个标题第一眼看上去像是一堆技术关键词的堆砌其实它代表了一类非常典型的全栈实战项目前端用Vue搭建商城页面后端在Django和Flask之间二选一开发工具用PyCharm最后交付一个能注册登录、逛商品、加购物车、下订单、后台管理的家居电商网站。这类项目最常见的场景是毕业设计、课程设计或者自学Python Web开发后的第一个“完整作品”。我做这类项目做过好几个版本从一开始只敢用Django写模板渲染的传统单体应用到后来改成Django REST Framework Vue 3前后端分离中间踩了不少坑。这篇博文就把我在实现“网上家居商城系统”时最完整的一套方案拆开讲清楚从环境配置、数据建模到后端接口、前端页面再到联调部署和常见报错处理。那这个商城到底包含哪些功能以家居行业为例核心业务线其实很清晰用户看到商品列表按分类筛选沙发、床垫、灯具、收纳柜点进详情页看图文介绍加入购物车结算生成订单最后模拟支付。用户端之外还有一个管理后台用来维护商品、处理订单状态。别看这些功能听起来“很常规”真要把它们串成一个前后端分离的系统牵涉到的知识点密度相当高ORM建模、JWT鉴权、跨域代理、路由守卫、状态管理……每一个环节都能单独写几千字。这篇内容适合谁第一类是准备做毕业设计、需要快速搭出完整系统的在校生第二类是学完Python基础、想通过真实项目把知识串起来的自学者第三类是工作中需要从零搭建一个内部电商或商品展示系统想参考前后端分离方案的开发者。后端的示例代码我以Django为主Flask和FastAPI我会在关键地方做横向对比毕竟标题里两个框架都写了很多同学确实会在这个选择题上纠结。2. 开发前的环境准备Python、PyCharm、Vue一个也不能少2.1 Python虚拟环境与 Django 工程初始化不管选Django还是Flask第一步永远是先把Python环境收拾干净。这里强烈建议不要直接用系统自带的Python解释器一定要建虚拟环境。虚拟环境的作用就是给每个项目一个独立的依赖容器不然你装了Django 5另一个老项目需要Django 2.2环境直接打架搞到后面连pip list都不敢看。在PyCharm里建项目时可以选择“New environment using Virtualenv”Python版本建议3.10或3.11。Django目前对3.12也支持得不错但如果第一次配环境用3.10最省心很多第三方库的预编译包对3.10支持最全。选好解释器后打开Terminal确认一下python --version pip list接着安装后端核心依赖pip install django djangorestframework django-cors-headers django-filter pip install djangorestframework-simplejwt pillowpillow必装商品图片上传、缩略图生成都靠它。django-cors-headers是为了解决Vue开发服务器localhost:5173请求Djangolocalhost:8000时的跨域拦截后面联调时如果没有它前端接到的一律是CORS报错。工程初始化django-admin startproject house_mall cd house_mall python manage.py startapp apps.user python manage.py startapp apps.product python manage.py startapp apps.order我个人习惯把业务App统一放在apps目录下这样项目结构更清晰。不要把所有Model堆在同一个App里用户、商品、订单本来就是三个独立业务域拆开写后续维护会轻松很多。2.2 Vue 3工程搭建与PyCharm联动配置前端部分我用Vue 3 Vite不用Vue CLI。Vite启动速度快配置简洁现在的Vue官方脚手架默认也是Vite。在PyCharm的Terminal里执行npm create vuelatest house_front注意这个命令之后所有安装包都会走npm网络慢就配一下国内镜像源不然一个node_modules能下载半小时。进入工程后启动开发服务器cd house_front npm install npm run devVue工程创建出来后先别急着写页面有几个基础配置必须提前做好第一跨域代理。在vite.config.js里配置export default defineConfig({ server: { host: 127.0.0.1, port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })这个代理的意义在于开发阶段你不需要在Django里开CORS白名单所有以/api开头的请求都被Vite悄悄转发给后端浏览器以为请求是同源的联调阶段能少掉一半的跨域烦恼。当然生产环境还是要正经配Nginx的这个我后面展开讲。第二安装前端必备依赖npm install axios pinia vue-router4axios负责发HTTP请求pinia管理全局状态vue-router管路由。如果你用的还是Vue2思维去装VuexVue 3项目里我更推荐pinia语法更轻、TypeScript支持更好后面讲购物车状态时会演示。第三配置PyCharm。PyCharm其实不需要额外装什么插件就能跑Vue但有几个建议打开File Settings Plugins安装Vue.js插件一般在PyCharm Pro版自带。社区版没有这个插件的话至少把Node.js插件装上。在Run/Debug Configurations里可以新增一个npm dev配置这样就不用每次去终端敲npm run dev了。2.3 一套顺手的基础配置CORS、DRF、JWT后端改settings.pyINSTALLED_APPS [ ... rest_framework, corsheaders, django_filters, apps.user, apps.product, apps.order, ] MIDDLEWARE [ ... corsheaders.middleware.CorsMiddleware, ]如果开发阶段依赖Vite代理CORS不用开。但有时候你会直接用Postman测接口或者前端不经过代理那我建议还是把CORS配置写上省得到处找问题CORS_ALLOWED_ORIGINS [ http://127.0.0.1:5173, http://localhost:5173, ]注意CorsMiddleware尽量放在MIDDLEWARE列表靠前的位置这是官方建议否则某些情况下响应头加不进去。REST Framework配置REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_FILTER_BACKENDS: [ django_filters.rest_framework.DjangoFilterBackend, rest_framework.filters.SearchFilter, rest_framework.filters.OrderingFilter, ], }JWT的作用是让前端在登录成功之后拿到一个token后续所有请求都带着这个token访问受保护的接口。相比Django默认的Session认证JWT天然适合前后端分离。这里要给一个我自己的明确建议如果你做的是纯后端API用DRF simplejwt如果你做的是小型企业内部系统Flask JWT也是一样套路只是没有Django的Admin可以白嫖。3. 数据建模让家居商品真正在数据库里落地3.1 五大核心模型设计与字段思路建模是商城系统的地基。这一步如果设计得不好后面写接口和页面时会不断返工。我在这个项目里设计了五个核心模型用户、分类、商品、购物车、订单外加一个订单明细表和商品SKU表。先看最核心的models.py示例from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone models.CharField(max_length11, uniqueTrue, blankTrue) avatar models.ImageField(upload_toavatars/, nullTrue, blankTrue) is_vip models.BooleanField(defaultFalse) class Meta: db_table user class Category(models.Model): name models.CharField(max_length50) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.SET_NULL) sort models.IntegerField(default0) class Meta: db_table category class Product(models.Model): name models.CharField(max_length200) category models.ForeignKey(Category, on_deletemodels.PROTECT, related_nameproducts) cover models.ImageField(upload_tocovers/) detail_images models.JSONField(defaultlist, blankTrue) description models.TextField(blankTrue) price models.DecimalField(max_digits10, decimal_places2) sales models.IntegerField(default0) is_active models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table product ordering [-created_at] class SKU(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameskus) spec models.CharField(max_length100) stock models.IntegerField(default0) price models.DecimalField(max_digits10, decimal_places2) class Meta: db_table sku unique_together (product, spec) class CartItem(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) sku models.ForeignKey(SKU, on_deletemodels.CASCADE) quantity models.IntegerField(default1) class Meta: db_table cart_item class Order(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 已支付), (shipped, 已发货), (completed, 已完成), (cancelled, 已取消), ] order_no models.CharField(max_length40, uniqueTrue) user models.ForeignKey(User, on_deletemodels.PROTECT) total_amount models.DecimalField(max_digits12, decimal_places2) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) address models.TextField() created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table order class OrderItem(models.Model): order models.ForeignKey(Order, related_nameitems, on_deletemodels.CASCADE) sku models.ForeignKey(SKU, nullTrue, on_deletemodels.SET_NULL) product_name models.CharField(max_length200) spec models.CharField(max_length100, blankTrue) price models.DecimalField(max_digits10, decimal_places2) quantity models.IntegerField() class Meta: db_table order_item几个设计要点解释一下。Category的parent字段支持二级分类比如“卧室家具”下面挂“床垫”、“衣柜”用自关联实现。Product的category用了on_deletemodels.PROTECT这是有意为之如果某个分类下还有商品后端就不允许直接把分类删掉否则商品变成无家可归的孤儿数据。Order.user也用了PROTECT防止用户被删后订单查不到归属。SKU是“库存保有单位”一个商品可能有“1.8m床/灰色”和“1.8m床/米白”两个SKU各自管各自的库存和价格。很多初学项目会直接把颜色和尺寸塞进一个字符串后来做购物车才发现同一个商品不同规格没法分别加减库存只能回来拆表。3.2 查询、删除对象与软删除的实战细节Django ORM的查询和删除很多人会写但细节坑不少。热搜词里有个“django执行查询-删除对象”我觉得有必要把这一块单独拎出来讲透。查单个对象product Product.objects.get(id13)这行代码在数据不存在时会抛出DoesNotExist在数据超过一条时会抛出MultipleObjectsReturned。实战中我在接口里很少直接get而是用filter().first()来拿可空结果避免异常导致500。比如product Product.objects.filter(id13, is_activeTrue).first() if product is None: return Response({msg: 商品不存在}, status404)删除对象最直接的是product.delete()delete()的返回值是一个元组(总删除行数, {数据表: 行数})很多人不知道这一点。更常见的问题是Django默认联级删除。如果你删一个Product其关联的SKU会因为外键on_deletemodels.CASCADE被一并删除甚至购物车里还引用着这个SKU也会被拖着删。所以在商城这种有交易数据的系统里我更推荐“软删除”策略给Product加is_active字段下架就置False不真删。product Product.objects.filter(id13).first() product.is_active False product.save()商品列表查询里默认过滤is_activeTrue前台立刻看不到这个商品但后台还能查到历史订单也不受影响。这是商城系统里最稳妥的做法真正的硬删除只留给超管后台去处理。3.3 用事务保护订单与库存下单的核心逻辑不是insert一条订单记录那么简单而是“扣库存 创建订单 生成订单项”必须同时成功或同时失败。我的做法是把整个下单流程包在transaction.atomic()里from django.db import transaction from django.db.models import F transaction.atomic def create_order(user, address, items): order Order.objects.create( order_nogenerate_order_no(), useruser, total_amount0, addressaddress, ) total 0 for item in items: sku SKU.objects.select_for_update().get(iditem[sku_id]) if sku.stock item[quantity]: raise ValueError(f{sku.product.name} 库存不足) sku.stock - item[quantity] sku.save() OrderItem.objects.create( orderorder, skusku, product_namesku.product.name, specsku.spec, pricesku.price, quantityitem[quantity], ) total sku.price * item[quantity] order.total_amount total order.save() return orderselect_for_update()是悲观锁它在数据库层面锁定选中的SKU行直到事务提交或回滚才释放。并发下单时两个用户同时买同一件商品后到的那个人必须等前面的事务结束才能读到最新库存。如果不加锁在并发高一点的场景下会出现超卖库存剩1件两个人同时查出库存满意同时扣减最后卖出2件。这是电商的致命bug从一开始就要规避。生成订单号我这里简单处理用时间戳加随机串import time import random def generate_order_no(): return time.strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999))4. 后端接口实战从注册登录到下单支付4.1 用户认证与权限控制用户模块是商城的入口。注册接口需要校验用户名、密码、手机号登录接口校验通过后发放JWT token。simplejwt的默认配置已经很够用在urls.py里挂上路由from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns [ path(api/token/, TokenObtainPairView.as_view(), nametoken_obtain_pair), path(api/token/refresh/, TokenRefreshView.as_view(), nametoken_refresh), ]自己写注册接口时用Django自带的User模型创建用户很省事from django.contrib.auth.hashers import make_password from .serializers import RegisterSerializer from rest_framework.views import APIView from rest_framework.response import Response class RegisterView(APIView): def post(self, request): ser RegisterSerializer(datarequest.data) ser.is_valid(raise_exceptionTrue) data ser.validated_data if User.objects.filter(usernamedata[username]).exists(): return Response({msg: 用户名已存在}, status400) User.objects.create( usernamedata[username], passwordmake_password(data[password]), phonedata.get(phone, ), ) return Response({msg: 注册成功}, status201)密码必须用make_password加密存储拿到原始密码直接存库是最低级的错误。Django默认的哈希算法是PBKDF2安全性对于毕设和一般商用都够。用Flask的话对应方案是werkzeug的generate_password_hash。订单、购物车、个人中心这些接口都要用JWT保护。DRF的permission_classes直接搞定from rest_framework.permissions import IsAuthenticated class CartView(APIView): permission_classes [IsAuthenticated] def get(self, request): items CartItem.objects.filter(userrequest.user) ...前端登录后把token放在Authorization请求头里格式是“Bearer xxx”。JWT的过期时间默认是5分钟太短refresh token默认1天。实际开发中我把access token改成30分钟refresh token改成7天配置在settings.py里from datetime import timedelta SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(minutes30), REFRESH_TOKEN_LIFETIME: timedelta(days7), }4.2 商品检索、排序与详情商品接口是走读接口对所有人开放。用DRF的ViewSet可以少写很多样板代码from rest_framework import viewsets from .models import Product from .serializers import ProductSerializer class ProductViewSet(viewsets.ReadOnlyModelViewSet): queryset Product.objects.filter(is_activeTrue) serializer_class ProductSerializer search_fields [name, description] ordering_fields [price, sales, created_at] filterset_fields [category]配好前面的django_filter之后前端请求GET /api/products/?search沙发ordering-salescategory3秒级拿到“沙发里按销量排序”的结果。为什么用ReadOnlyModelViewSet因为前台用户不需要写操作商品新增和修改都在后台管理里做这部分单独开接口和页面。商品详情需要把SKU列表一起返回避免前端为了查规格再发一次请求。Serializerclass SKUSerializer(serializers.ModelSerializer): class Meta: model SKU fields [id, spec, price, stock] class ProductDetailSerializer(serializers.ModelSerializer): skus SKUSerializer(manyTrue, read_onlyTrue) class Meta: model Product fields [id, name, cover, detail_images, description, price, sales, skus]注意detail_images用的是JSONField里面存的是图片URL列表。这个字段查询快、写入方便不需要单独建一张图片表。对小体量项目来说性价比极高。4.3 购物车与订单状态机购物车有两种实现方式一种是纯前端localStorage整个购物车都存在浏览器里用户换成别的设备就没了另一种是后端数据库表登录后随时同步。商城系统我建议走后端数据库方案。原因很简单结算时要创建订单订单需要依赖购物车里的SKU和数量数据放在后端库存校验和价格计算都更可靠。用户未登录时不开放购物车接口前端引导去登录登录后购物车数据以数据库为准。购物车加商品class AddCartView(APIView): permission_classes [IsAuthenticated] def post(self, request): sku_id request.data.get(sku_id) quantity int(request.data.get(quantity, 1)) sku SKU.objects.filter(idsku_id).first() if sku is None: return Response({msg: 规格不存在}, status404) cart_item, created CartItem.objects.get_or_create( userrequest.user, skusku, defaults{quantity: quantity}, ) if not created: cart_item.quantity quantity cart_item.save() return Response({msg: 已加入购物车})订单的状态机是pending → paid → shipped → completed以及pending可以到cancelled。后台管理员发货用户确认收货或自动完成取消订单有半小时内的人工限制。这个状态流转我用一张表维护当前状态可执行操作下一状态待支付支付已支付待支付取消已取消已支付发货已发货已发货确认收货已完成状态判断一定要在后端校验不能只靠前端按钮控制。最简单的做法是写一个方法检查当前状态是否允许跳转def transition_order(order, target_status): allowed { pending: [paid, cancelled], paid: [shipped], shipped: [completed], } if target_status not in allowed.get(order.status, []): raise ValueError(f订单状态不允许从 {order.status} 变为 {target_status}) order.status target_status order.save()4.4 支付模拟与实时通知的一个轻量做法真实接入支付宝或微信支付需要企业资质、应用审核、回调地址对于学习和演示项目来说太重了。通常的做法是做一个“模拟支付”接口用户点击支付弹出一个虚拟收银台确认后后端直接把订单状态改成paid同时伪造一个支付流水号记录。class MockPayView(APIView): permission_classes [IsAuthenticated] def post(self, request, order_no): order Order.objects.filter( order_noorder_no, userrequest.user, statuspending, ).first() if order is None: return Response({msg: 订单不可支付}, status400) order.status paid order.save() return Response({msg: 支付成功, order_no: order.order_no})至于页面如何感知支付成功大多数毕设用轮询前端点击支付后每2秒查一次订单状态直到变成paid。我做过一个稍微进阶一点的方案用SSEServer-Sent Events。它就是后端主动往一个HTTP连接里推数据很适合“订单支付成功通知”这种单向实时场景。Vue前端用EventSource接const source new EventSource(/api/order/notify/ orderNo) source.onmessage (e) { const data JSON.parse(e.data) if (data.status paid) { // 跳转支付成功页 source.close() } }Django这边需要借助StreamingHttpResponse实现。新项目如果选FastAPISSE支持更原生异步接口配合sse-starlette几行就能实现。Flask也能做但需要gevent配合长连接复杂度更高。所以如果你的实时通知频率很低我反而建议老老实实轮询简单可靠不会把连接资源拖死。5. 前端Vue实现路由、组件与状态管理5.1 路由规划和动态路由权限前端页面结构看着多塞到路由里其实就三类公开页面、需登录页面、管理员页面。公开页面包括首页、分类页、商品详情页需登录页面是购物车、结算、个人中心管理员页面是后台商品管理和订单管理。vue-router的meta字段用来标记权限非常方便import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: home, component: () import(/views/HomeView.vue) }, { path: /product/:id, name: product-detail, component: () import(/views/ProductDetail.vue) }, { path: /cart, name: cart, component: () import(/views/CartView.vue), meta: { requiresAuth: true } }, { path: /admin, name: admin, component: () import(/views/admin/AdminLayout.vue), meta: { role: admin } }, ]路由守卫里检查token和角色router.beforeEach((to, from, next) { const token localStorage.getItem(access_token) const role localStorage.getItem(user_role) if (to.meta.requiresAuth !token) return next(/login) if (to.meta.role admin role ! admin) return next(/) next() })为什么用动态路由因为admin模块的路由表很长普通用户根本不应该拿到。管理员的菜单和页面可以在登录后按角色动态注册if (role admin) { router.addRoute({ path: /admin/product, component: () import(/views/admin/ProductManage.vue) }) }这样做的额外好处是首屏js拆包普通用户浏览器不会加载管理员页面代码心理上也踏实一些。5.2 商品卡片、轮播图与插槽复用前端最容易写出重复代码的地方就是商品卡片。首页的“热门推荐”、分类页的“全部商品”、搜索结果页都是“一个封面图 名称 价格 销量”的卡片结构。我封装了一个ProductCard组件template div classproduct-card click$router.push(/product/${product.id}) el-image :srcproduct.cover fitcover lazy / div classname{{ product.name }}/div div classprice span classnow¥{{ product.price }}/span span classsales已售 {{ product.sales }}/span /div /div /template script setup defineProps({ product: { type: Object, required: true } }) /script列表页循环使用即可数据源不同展示结构完全复用。插槽这块也值得一提。商城页面渲染数据时经常要处理“加载中”、“空数据”、“出错”三种状态。我封装了一个BaseState组件用插槽让调用方自定义内容template div classbase-state img :srcicon / slot namemessage暂无数据/slot slot nameaction/slot /div /template调用的时候BaseState template #message购物车还是空的/template template #action el-button typeprimary click$router.push(/)去逛逛/el-button /template /BaseState插槽的意义在于把状态容器和具体内容解耦封装一次到处用。热搜词里“vue插槽”被搜得很多说明不少人还是搞不清插槽的应用场景这里就是个很自然的例子。5.3 用Pinia管理购物车和用户态购物车状态是全局的用户从详情页加购跳去购物车页要能立刻看到用户在购物车页改数量顶栏角标也要同步变。这个场景非常适合用状态管理库。Pinia定义购物车storeimport { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: [], totalCount: 0 }), getters: { totalPrice: (state) state.items.reduce((sum, item) sum item.price * item.quantity, 0) }, actions: { async fetchCart() { const res await http.get(/api/cart/) this.items res.items this.totalCount res.items.reduce((sum, item) sum item.quantity, 0) }, async addItem(skuId, quantity 1) { await http.post(/api/cart/add/, { sku_id: skuId, quantity }) this.fetchCart() } } })顶栏购物车图标只需要读取totalCount这个数字会随着任何页面的加购操作自动更新这就是状态管理的核心价值。用户登录后在应用启动或路由守卫里调用fetchCart刷新页面购物车也不丢。用户态我放在另一个store里export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(access_token) || , userInfo: null }), actions: { async login(form) { const res await http.post(/api/token/, form) this.token res.access localStorage.setItem(access_token, res.access) }, logout() { this.token this.userInfo null localStorage.removeItem(access_token) } } })axios拦截器统一从store里取token加进去避免每个接口手动写headerhttp.interceptors.request.use((config) { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config })6. 联调部署与踩坑记录6.1 跨域、代理和前端鉴权拦截开发和联调阶段最容易炸的就是跨域和token失效。前端请求路径用相对路径写比如/api/products/开发环境走Vite代理到Django生产环境走Nginx到后端代码都不用改动。这是很多人不知道的小技巧不要把http://127.0.0.1:8000写死在axios里以后换域名会想哭。axios响应拦截器里我统一处理401http.interceptors.response.use( (res) res.data, async (error) { if (error.response error.response.status 401) { const refresh localStorage.getItem(refresh_token) if (refresh) { try { const res await http.post(/api/token/refresh/, { refresh }) localStorage.setItem(access_token, res.access) error.config.headers.Authorization Bearer ${res.access} return http.request(error.config) } catch (e) { // refresh token 也失效强制重新登录 localStorage.clear() window.location.href /login } } } return Promise.reject(error) } )这套“双token刷新”机制逻辑是access过期了拿refresh换新的然后重放原来失败的请求。用户无感知地续期这是体验比较好的做法。如果不做刷新用户逛着逛着突然看到接口全挂体验很糟。6.2 生产环境部署Django后端Vue静态页面本地开发跑通了最后一步是部署上线。Vue这边执行npm run build产出dist目录。Django这边两步第一setting里加白名单和静态目录ALLOWED_HOSTS [你的域名或IP] import os STATIC_ROOT os.path.join(BASE_DIR, staticfiles)第二收集静态文件和迁移数据库python manage.py collectstatic python manage.py migrate启动Django使用gunicornpip install gunicorn gunicorn house_mall.wsgi:application --bind 0.0.0.0:8000 --workers 3Flask部署也差不多gunicorn配wsgi_app即可。然后Nginx做反向代理server { listen 80; server_name your_domain; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/house_front/dist; try_files $uri $uri/ /index.html; } }try_files这行很关键vue-router的history模式刷新页面时会请求不存在的路径Nginx必须把请求全部落回index.html由前端路由接管。静态文件收集这一步我踩过一次坑在本地开发时图片能正常显示部署到服务器后所有图片都是404因为开发环境下Django替你把/media的静态文件直接喂给了浏览器生产环境必须由WEB服务器处理。用whitenoise可以省事一点但正规一点还是让Nginx单独代理一个/media路径指向MEDIA_ROOT目录。6.3 常见问题速查表写到最后我把这个项目里最容易出现的报错和排查思路总结成一张表每个问题都是我亲手遇过的现象原因解决方案前端页面无法访问后端控制台显示CORS报错后端未引入corsheaders或白名单缺失安装django-cors-headers添加白名单登录后请求/任意接口返回401access token过期或未加请求头给axios加拦截器统一携带Bearer token订单创建时提示“数据库被锁定”select_for_update等待超时检查事务是否及时提交避免锁表时间过长删除商品时报ProtectedError订单引用外键保护列表页先下架软删除后台手动清理关联Django后台图片上传失败未安装pillow或MEDIA配置缺失pip install pillow配置MEDIA_URL和MEDIA_ROOT前端打包后页面一片空白vue-router history模式刷新404Nginx里配置try_files中文乱码数据库字符集不是utf8mb4建库时指定charsetutf8mb4连接串加charset参数Vue项目npm install极慢网络原因配置npmmirror源商品详情页打开慢cover图太大用pillow生成缩略图前端加懒加载其中图片越界这个坑我要多啰嗦几句。后台传原图时如果不限制用户直接传一张5MB的照片前端渲染成几百KB的页面加载负担本地开发感觉不到部署到服务器后打开首页会肉眼可见地卡。我的做法是后端接口上传时统一压缩生成一个宽度600px的缩略图存库列表页只展示缩略图详情页再加载原图。最后再分享一个小技巧。很多同学做这类商城项目时会纠结要不要加秒杀、优惠券、聊天客服这些花哨模块我的建议是先把“商品-购物车-下单-支付-后台发货”这条主链路做到完全顺滑再考虑扩展。因为主链路会逼你完整走一遍建模、接口、前端、联调、部署这些才是面试和答辩时真正能讲出东西的部分。把核心链路跑通之后再往上面加数据可视化统计、推荐算法、消息推送都是顺理成章的事而且到时你手里已经有一套能复用的框架加功能只是往里面填积木而已。