开头直接切入。这个项目组合非常典型——Python做后端逻辑、Django做Web框架支撑、Vue负责前端交互、PyCharm作为统一开发环境目标明确搭一个服饰类的电商销售网站。很多人一听到“商城”就头大觉得要搞支付、搞秒杀、搞高并发但其实对于一个学习性质或中小体量的项目来说核心技术难点根本不在这。真正值得花时间拆解的是商品建模的颗粒度、购物车与订单的联动流程以及前后端联调时那些说不清道不明的跨域和认证问题。我前后带过不少用同类技术栈做毕业设计或个人项目的开发者发现大家踩的坑高度相似而网上教程又普遍只讲CRUD不讲为什么导致项目能跑但经不起追问。这篇文章就基于标题覆盖的技术栈围绕服饰商城这个具体场景还原我当时从零搭建同类项目的完整思考过程。会讲清楚为什么选这套组合、数据库里那些表到底怎么设计才算合理、接口怎么写才能不让前端同学骂人以及开发阶段容易忽略的配置细节。文章本身适配两类人一类是准备用DjangoVue做商城类项目的初学者另一类是已经会基础增删改查、但想理清完整业务闭环的开发者。1. 项目整体设计与技术选型思路1.1 项目要解决的核心问题服饰商城本质上是一个多商品SPU-SKU 购物车 订单状态流转的业务系统。SPU是标准产品单元比如某品牌连帽卫衣是一个SPU但同款卫衣有黑色、白色、M码、L码这些具体可下单的组合才是SKU库存量单位。如果不先把这个概念理清后面的数据库设计基本会乱。标题里的三个关键词其实对应项目的三层结构Python/Django负责后端的业务逻辑、数据建模、鉴权校验和接口输出是整个项目的地基。Vue负责前端的页面渲染和用户交互让商城拥有流畅的SPA体验。PyCharm作为开发工具统一管理Python环境、Vue前端工程和调试流程减少IDE切换带来的麻烦。这套组合解决的痛点很实在Django自带Admin后台、ORM和完整的安全中间件开发速度极快Vue通过组件化开发可以快速搭建商品列表、购物车、结算页等界面两者通过RESTful API沟通天然适合前后端分离的开发模式。整个项目做完既能覆盖后端建模能力又能体现前端交互水平非常适合作为综合实战项目。1.2 技术选型为什么是Django Vue PyCharm先说说Django。网上有个流传很广的说法Django太重不如Flask灵活。这个观点对一半。Flask确实轻量适合做极小的工具站点但商城项目涉及用户模块、商品模块、订单模块、后台管理、文件上传如果这些东西全部自己手动实现光是表单验证、CSRF防护和ORM映射就足以消耗大量时间。Django自带全套电池默认模块就能支撑完整业务这是效率上的巨大优势。再说Vue。Vue相比React更贴近传统后端开发者的思维习惯它的模板语法直观双向绑定能减少大量DOM操作代码。对于以Python为主的开发者来说Vue的学习曲线明显更友好。我见过不少人想一步到位学React结果被JSX和状态管理搞得筋疲力尽最终项目烂尾。最后圆回PyCharm。老实说用什么IDE不决定项目高度但好的工具能减少环境层面的干扰。PyCharm对于Django有专门的支持比如一键创建Django项目、内置manage.py命令面板、自动识别虚拟环境它同时也能打开Vue工程。这样一套工具解决两个项目的管理问题对新手来说是很舒服的组合。1.3 前后端分离还是服务端渲染的决策用Django的人经常会纠结到底用Django的模板系统Django Templates直接渲染页面还是单独搭Vue工程做前后端分离我给出的结论是如果项目定位是商城且你希望前端具备现代交互体验选前后端分离如果项目只求能跑通流程模板渲染更快。这背后是时间成本与技术收益的权衡。服务端渲染的路子里页面跳转依赖Django的url路由和TemplateView每个交互动作都会导致整页刷新购物车加减数量时体验尤其生硬。前后端分离则把页面渲染和路由跳转交给前端后端只输出JSON数据。以商品筛选为例前端拿到筛选条件后通过Axios请求后端接口拿到JSON数组再更新页面整个过程无刷新顺畅得多。这正是商城项目该有的交互体验。但前后端分离也有代价需要处理跨域、需要管理两套工程、需要约定接口规范。在后面的章节里我会具体讲到如何通过Django-cors-headers解决跨域以及如何通过Axios拦截器统一管理认证信息。做这个选择之前先把分工想清楚后面才不会手忙脚乱。2. 数据库设计与服饰商品模型拆解2.1 服饰商城的数据模型特性服装类目是出了名的难建模。普通商品可能一个商品ID对应一个价格、一个库存就够了但服装天生有多规格属性颜色、尺码可能还有版型。如果商品表里直接加一个颜色字段存成黑色,L码,白色,M码这样的拼接字符串那么后续查库存、算价格、生成订单时全部要解析字符串这属于给自己挖坑。服饰商城的数据模型至少应该拆成以下几张核心表Category分类男装、女装、童装、配饰等。Product产品/SPU商品的通用信息如名称、描述、主图。SKU库存单位具体可下单的规格记录特定颜色/尺码下的价格、库存、编码。Cart购物车存用户选择的SKU和数量。Order订单订单主表存收货地址、总金额、状态。OrderItem订单明细订单里的每一行商品快照SKU信息和下单价格。这里的关键不是表多而是业务职责被分割清楚。SKU表和Product表通过外键关联Cart表、OrderItem表和SKU关联这样无论前端如何选择规格组合后端查询都只是按外键过滤而已而不是字符串解析。2.2 从ER图到Django Model的落地设计好表关系后用Django的Model实现。实际写代码时Model可以这样组织简化示例class Category(models.Model): name models.CharField(max_length64, verbose_name分类名称) parent models.ForeignKey( self, nullTrue, blankTrue, related_namechildren, on_deletemodels.CASCADE, verbose_name父分类 ) class Product(models.Model): title models.CharField(max_length128, verbose_name商品标题) category models.ForeignKey( Category, related_nameproducts, on_deletemodels.CASCADE, verbose_name所属分类 ) main_image models.ImageField(upload_toproducts/%Y/%m/, verbose_name主图) description models.TextField(verbose_name商品描述, blankTrue) class SKU(models.Model): product models.ForeignKey( Product, related_nameskus, on_deletemodels.CASCADE, verbose_name所属商品 ) sku_code models.CharField(max_length64, uniqueTrue, verbose_nameSKU编码) color models.CharField(max_length32, verbose_name颜色) size models.CharField(max_length32, verbose_name尺码) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格) stock models.PositiveIntegerField(verbose_name库存) is_active models.BooleanField(defaultTrue, verbose_name是否上架)重点在SKU的unique约束和is_active字段。uniqueTrue确保同一个SKU编码不会重复创建is_active用于下架处理而不是直接把数据删除——电商项目里删除数据是大忌软删除能保留历史记录订单补发、售后追查、报表统计都依赖这些废数据。2.3 商品SKU设计的几个关键细节真正实战中SKU设计的坑集中在价格快照和库存扣减上。价格快照的意思是用户下单时商品价格被他写死到订单明细里。为什么不能下单时再去SKU表取价格因为价格是变动的今天99元明天可能109元如果订单明细不存快照查询历史订单时价格就全乱了。实现上很简单在OrderItem里加一个price_at_order models.DecimalField(...)字段即可。库存扣减也值得聊。我的建议是在购物车阶段不要扣库存只在订单创建成功后扣。为什么购物车本质是一个临时清单用户可能加进购物车后一直不买如果提前扣库存那些占着库存不付款的账号会把商品锁死其他真实买家反而买不到。但真到下单节点要在事务里先做SELECT ... FOR UPDATE锁定SKU行再检查库存并扣减避免两个请求同时把同一件商品的库存扣成负数。这个操作在MySQL里用select_for_update()实现Django的ORM原生支持后面实操章节会有示例。3. 后端核心业务功能实现3.1 商品检索与分类筛选商城项目里商品列表页一般有三块内容全部分类、推荐商品、排序方式。以点击分类筛选商品为例后端接口大致是# product/views.py from django.db.models import Q from .models import Product, SKU def product_list(request): category_id request.GET.get(category_id, ) sort request.GET.get(sort, default) products Product.objects.filter( category_idcategory_id, skus__is_activeTrue ).distinct() if sort price_asc: products products.order_by(skus__price) elif sort price_desc: products products.order_by(-skus__price) return render(request, product_list.html, {products: products})注意这里用了skus__price进行排序因为价格在SKU表里需要跨表查询。但我的实际经验是如果数据量上去了这种排序方式效率堪忧因为distinct()与order_by跨表时可能产生巨大的临时表。比较稳妥的做法是给Product表冗余一个min_price字段列表页直接用这个字段排序正则一些。网页端实现时筛选条件通常通过GET参数传递例如?category_id5sortprice_asc。前端用Axios发送这个GET请求拿到数据后渲染。接口设计不必封装得太复杂但流传一个约定分页参数统一为page和page_size返回结构统一为{ code: 0, data: {...} }这样前端封装请求时逻辑才能统一。这是项目后期调试效率的关键非常值得在一开始就坚持。3.2 购物车实现方案的取舍购物车的实现有三种常见方案存放在前端Cookies、存放在服务端Session、存放在数据库。这个选择直接影响体验和复杂度。方案一Cookies存购物车。优点是无状态刷新不丢缺点是无法跨端同步——手机和电脑的购物车各管各的而且Cookies有大小限制。方案二Session存购物车。基于服务端会话对Django来说内置支持完善但缺点是没有登录也可以靠Session ID维持购物车一旦Session过期购物车内容就没了。方案三数据库存购物车。这是目前主流结构清晰且支持多端同步。不需要登录也能建购物车但需要引入一个user_id或绑定一个临时标识比如浏览器指纹用户登录后再把临时购物车合并到正式账号下。以数据库方案为例购物车接口大概是# cart/views.py def add_to_cart(request): if request.method POST: sku_id request.POST.get(sku_id) quantity int(request.POST.get(quantity, 1)) cart, _ Cart.objects.get_or_create( userrequest.user, sku_idsku_id, defaults{quantity: quantity} ) if not _: cart.quantity quantity cart.save() return JsonResponse({code: 0, message: 已加入购物车})get_or_create在这里很好用首次添加创建一个新记录已存在则直接在数量上累加。但要注意一个细节在购物车中累加数量时应该判断累加后的总数是否超过SKU的库存。如果用户库存只有5件购物车已加3件用户又加3件合计6件此时应该提示库存不足而不是默默叠加。这个检查在购物车阶段做一次下单时重新检查一次双重保险。3.3 订单流程与状态机设计订单模块是商城项目的另一个难点核心在于状态流转不允许乱跳。我习惯给订单定义一组常量和一张清晰的状态表ORDER_STATUS [ (PENDING, 待付款), (PAID, 已支付), (SHIPPED, 已发货), (COMPLETED, 已完成), (CANCELLED, 已取消), ]流程上用户提交订单 - 待付款 - 支付真实对接支付网关复杂度高大多数实战项目用模拟支付/回调接口代替- 已支付 - 管理员发货 - 已发货 - 用户确认收货 - 已完成。同时也要处理好边界状态未付款订单超时自动取消这是通过Django的定时任务或Celery实现的但在学习阶段可以不引入任务队列直接用一个管理命令定时扫描超时订单即可。实现订单创建时事务是必须的。下面是简化版的订单创建核心逻辑from django.db import transaction from django.db.models import F transaction.atomic def create_order(request): sku_ids request.POST.getlist(sku_ids) cart_items Cart.objects.filter(userrequest.user, sku_id__insku_ids) # 先锁定SKU行防止并发超卖 sku_list SKU.objects.select_for_update().filter(pk__incart_items.values_list(sku_id, flatTrue)) for cart_item in cart_items: sku sku_list.get(pkcart_item.sku_id) if cart_item.quantity sku.stock: raise ValueError(f商品{sku.sku_code}库存不足) # 创建订单和明细 order Order.objects.create(userrequest.user, total_amount..., statusPENDING) for cart_item in cart_items: OrderItem.objects.create( orderorder, skucart_item.sku, pricecart_item.sku.price, quantitycart_item.quantity, ) # 扣减库存用F表达式避免并发覆盖 SKU.objects.filter(pkcart_item.sku_id).update(stockF(stock) - cart_item.quantity) cart_item.delete() return order两个关键点select_for_update()会把SELECT命中的行锁住直到事务结束。事务里有两条并发请求同时执行到库存检查一个会等另一个执行完再继续从而避免两个线程同时读到20件库存、同时减到15件的覆盖问题。扣库存用的是F(stock) - cart_item.quantity这是数据库层的原子操作。如果直接取出来减再赋值并发场景容易由于先读再写的时序导致覆盖F表达式则把减法操作推到数据库执行数据库的锁会确保字段的加减是串行的。这是老生常谈但真的重要商城类项目并发一上来这个细节直接决定库存准不准。4. 前端Vue页面实现与前后端接口联调4.1 前端工程结构与路由规划Vue前端不适合所有页面堆在单个App.vue里至少要拆出组件目录和视图目录。以商城为例合理的结构是src/ api/ # 接口封装 assets/ # 静态资源 components/ # 通用组件如Header、GoodsCard router/ # 路由定义 store/ # Vuex状态管理 views/ # 页面级组件 Home.vue ProductList.vue ProductDetail.vue Cart.vue Checkout.vue OrderList.vue路由规划要把商品浏览和订单流程分离让人一眼看懂跳转关系。在router/index.js里常见的配置方式const routes [ { path: /, component: Home }, { path: /product/list, component: ProductList }, { path: /product/detail/:id, component: ProductDetail }, { path: /cart, component: Cart }, { path: /checkout, component: Checkout, meta: { requiresAuth: true } }, { path: /order/list, component: OrderList, meta: { requiresAuth: true } } ];注意/product/detail/:id这种动态路由商品详情页必须靠URL里的ID去请求后端数据而不能在导航时把整个商品对象塞进全局状态里。为什么因为用户可能会刷新详情页刷新后全局状态被清空如果此时没有ID参数的支撑页面就会白屏。这种动态路由和全局状态的配合是前后端分离项目最容易犯的低级错误提前做对能省大量调试时间。4.2 接口封装与Vuex状态管理前后端对接时前端需要统一封装HTTP请求工具避免每个页面都写一遍axios的header配置和错误处理。基础封装里至少要做两件事统一处理BaseURL与超时import axios from axios; const service axios.create({ baseURL: process.env.VUE_APP_API_BASE || http://127.0.0.1:8000/api, timeout: 10000, }); service.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });统一处理响应码与错误提示service.interceptors.response.use( (response) { const res response.data; // 约定后端返回 { code: 0, data: ... }code非0视为业务错误 if (res.code ! 0) { alert(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res.data; }, (error) { if (error.response error.response.status 401) { // 未登录或token失效跳转登录页 router.push(/login); } return Promise.reject(error); } ); export default service;Vuex方面联调中最值得维护的全局数据是购物车数量。用户每次加入购物车后购物车徽标上的数字都要全局更新。做法是购物车的增删改接口成功后调用Vuex里的fetchCartCountaction把最新数量同步到state。这样无论用户在哪个页面操作购物车顶部导航栏的角标都是准确的。如果不做全局状态管理就得在多个组件里手动同步数字改起来极其痛苦。4.3 前后端联调中的三个高发坑前后端分离项目里很多问题不是某个接口写错了代码而是沟通和约定层面的细节。根据我的实测经验三个高发坑值得重点记录坑一跨域配置。Django默认不允许来自其他域名的浏览器请求后端需要配置跨域。最省事的方案是安装django-cors-headers在settings.py中启用并加白名单INSTALLED_APPS [ ..., corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ..., ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, ]开发阶段如果把前端跑在localhost:8080、后端跑在8000不配置这个前端所有请求都会在浏览器控制台报blocked by CORS policy错误。新手容易怀疑代码逻辑其实只是跨域配置缺失。坑二Cookie与Token的认证选择。前后端分离的环境下主流做法是用JWTJSON Web Token认证因为同一套后端可能对应多个前端Web端、管理端JWT天然支持无状态、跨域。Django生态里常用的库是djangorestframework-simplejwt配好后在请求头里携带Authorization: Bearer token即可。流程上用户登录成功后前端把token存到localStorage或sessionStorage之后每次请求都自动带上这个token。上面axios拦截器里的config.headers.Authorization ...就是干这个的。坑三前端拿不到文件上传后的URL。用户上传商品图片时后端返回的往往是文件的相对路径比如/media/products/2024/11/xxx.jpg。前端如果直接拿这个路径去渲染img会发现图片加载404。原因在于这个路径需要拼接后端的地址例如http://127.0.0.1:8000/media/...。更规范的做法是后端接口返回完整的绝对URL。在Django的序列化代码里可以用request.build_absolute_uri(obj.image.url)来生成完整地址。这个小点不解决测试阶段会浪费大量时间排查。5. 开发环境搭建与PyCharm配置实战5.1 从零搭建Django后端工程项目结构上我建议一个仓库里包含backend/和frontend/两个子目录分别装Django工程和Vue工程。这种布局在PyCharm里打开项目根目录后能同时管理两个部分不会出现一套IDE只能开一个项目的割裂感。后端搭建步骤大致如下创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install django djangorestframework django-cors-headers pillowpillow是Django处理ImageField的必备库忘记装它会导致图片上传页面直接报错。创建Django项目和Appdjango-admin startproject config . python manage.py startapp apps这里解释一个小习惯项目名用config而不是django_vue之类的名字因为config更贴近配置文件目录的语义同时也避免与后续创建的App名称冲突。在settings.py中注册rest_framework和APP配置数据库。如果使用默认SQLite注意Sqlite的并发性能在正式部署阶段有限学习阶段先用它跑通逻辑没问题。如果要用MySQL务必装好pymysql驱动并在__init__.py里做初始化网上关于这个报错Did you install mysqlclient?的帖子非常多核心原因就是初始化缺失。配置Django REST framework时可以先写一个简单的视图试试水。比如# apps/views.py from rest_framework.decorators import api_view from rest_framework.response import Response api_view([GET]) def health(request): return Response({code: 0, data: ok})在urls.py里path(api/health, health)挂载然后后端跑起来python manage.py runserver 0.0.0.0:8000浏览器访问http://127.0.0.1:8000/api/health能返回JSON就说明后端环境OK了。5.2 Vue前端工程配置与代理前端搭建建议直接用Vue CLI或Vite。以Vue 3 Vite为例创建工程npm create vuelatest frontend cd frontend npm install npm run dev开发阶段前端默认跑在http://localhost:5173它会请求http://localhost:8000的接口所以跨域配置需要照顾到5173端口。另外除了后端配置CORS白名单在Vite工程里也可以用代理解决跨域// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, }, }, }, });配置后前端请求/api/product/list会被代理转发到http://127.0.0.1:8000/api/product/list。浏览器看起来是同一个源发起的请求从而绕开跨域限制。我发现同时配好后端CORS白名单和前端代理是最稳的组合即便其中一个配置失效也不至于整个联调瘫痪。5.3 图片上传与媒体文件处理商城项目离不开商品图片而Django的图片上传配置极其常见却极其容易忽略MEDIA_URL的设置。正确配置方式为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_rootsettings.MEDIA_ROOT)如果不加这行Django的runserver默认不会处理media目录下的静态文件上传图片成功后前端会拿到一个无法访问的URL。这个问题在部署到Nginx之前几乎必现先配置好再往下走省心很多。上传接口的序列化里要注意前端传的是FormData格式而不是JSON字符串。如果你用api_view([POST])接收请求对象里不会自动带json字段需要从request.FILES里取文件。实战中很多上传失败的报错都源于前端没有设置Content-Type: multipart/form-data或是后端尝试从request.data读取文件。规范做法是前端用new FormData()然后交给Axios处理不需要手动指定HeaderAxios会按FormData自动生成multipart请求头。6. 常见问题排查与部署上线心得6.1 高频问题速查表把项目做完整的过程中我梳理了一张高频问题对照表很多坑是DjangoVue组合下反复出现的。集中列出来方便对照排查问题现象常见原因解决建议前端请求接口报CORS错误后端未配置CORS白名单安装并配置django-cors-headers图片上传后访问404未配置MEDIA_URL或未在urls注册静态路径在urls.py添加static()支持登录后接口仍报401前端未在请求头带token检查axios拦截器是否统一加Authorization商品详情页刷新后白屏把参数存在全局状态而非URL路由改用路由参数/product/detail/:id数据库出现database is lockedSQLite并发写入冲突学习阶段减少写并发部署换MySQL/PostgreSQL前端请求到达后端但数据不更新跨域配置了但未刷新后端进程修改settings.py后重启runserver虚拟环境装了Django但命令找不到PyCharm的Terminal未激活虚拟环境在Terminal执行venv\\Scripts\\activateWindows或重新选择解释器6.2 部署上线的实操建议部署这一步在标题里没直接体现但完整项目实施必然绕不开。常见的部署路径是前端打包成静态文件交给Nginx托管静态资源并转发API请求后端用Gunicorn跑Django服务通过UnixSocket与Nginx互通数据库从SQLite切到MySQL/PostgreSQL。核心流程大致是前端执行npm run build生成dist目录。把dist内容复制到服务器的某个目录比如/var/www/shop-web。Nginx配置中把location /指向dist目录把location /api和location /media代理到后端服务。Django的DEBUG改为False配置ALLOWED_HOSTS同时用collectstatic收集静态文件Admin后台样式需要。部署阶段有个常见坑Django后端与前端页面不在同一个域名下时前端请求的baseURL如果写死成http://127.0.0.1:8000部署后就全完了。建议把API地址配置到环境变量如.env.production文件构建时读取这样不同环境打包互不干扰。Vite对.env文件的处理是自动的前缀为VITE_的变量会在构建时被打进产物所以配置VITE_API_BASE最合适。6.3 后续值得深化的优化方向项目能跑通只是第一阶段如果想让它在简历或者作品集里更有说服力可以在以下几个方向继续深挖方向一接入Redis做Session共享或缓存。Django默认的Session存在数据库里用户量稍大就会成为瓶颈。引入Redis做缓存后端后不仅Session能存缓存里商品列表页的接口也可以做响应缓存减少数据库压力。方向二订单超时取消。引入Celery Redis做异步任务待付款订单超过30分钟自动取消并恢复库存。这一步虽然不增加购买功能但真实商城有明确的时效要求做出来很加分。方向三后台管理系统。Django Admin是开箱即用的可以把它作为运营后台管理员直接在上面录入商品、修改库存。但如果想做得更好看、更可控可以用Vue单独做一个管理端界面把Admin的交互逻辑替换成表格、表单、弹窗的组合。方向四订单支付回调模拟。真实支付需要企业资质和沙箱账号个人项目申请门槛高。但你可以预先定义一套回调接口在项目里用一个模拟支付页面直接触发订单状态从待付款变成已支付并附上回调数据签名说明。这套逻辑把真实支付网关的时序演示清楚就能向面试官证明你理解支付流程而不是只会调库。我个人实际带完同类型项目的体会是这个项目90%的精力会花在业务设计和联调上而不是写代码。SKU粒度设计、购物车要不要合并、订单状态机怎么流转、权限校验放前端还是后端这些决策比单点技术实现更考验综合能力。如果你正在做或者准备做类似的项目我强烈建议在动手前花一个晚上把数据库的表和字段先画在一张纸上搞清楚每个表之间的外键关系。没有这一步后续所有开发都可能陷在改表的泥潭里。最后再说一个具体的小技巧项目里所有对外输出的时间字段建议统一用datetime类型并在前端格式化而不是后端直接输出字符串。比如Django的DateTimeField在序列化时默认输出ISO格式前端dayjs或moment处理起来很方便。如果后端List API里有时间字符串乱码的感觉多数是序列化时用了str()而不是ISO格式。这个看似不起眼的约定能让前后端沟通顺畅不少。