恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python+Django+Vue3家具商城系统实战:从数据库设计到部署
首页
资讯中心
/
Python+Django+Vue3家具商城系统实战:从数据库设计到部署
Python+Django+Vue3家具商城系统实战:从数据库设计到部署
发布时间:2026/10/5 3:10:18
这两年不少朋友问我要一个能直接落地的电商项目参考尤其是Python后端配合Vue3管理端的组合。去年我完整实现过一套家具商城系统从商品浏览、购物车到下单、订单管理再到后端管理后台全流程都跑通了。今天把设计思路和实现细节拆出来讲重点聊三件事为什么选Python Vue3这套组合、数据库和核心业务怎么设计、联调和部署阶段有哪些坑。如果你正准备做类似的商城项目或者正在搞毕业设计、个人作品这篇可以直接当参考地图用。先交代一下项目背景这是一套典型的B2C家具商城前台给普通用户看后台给运营人员管。家具类目有个特点——商品信息比普通日用品复杂包含材质、尺寸、风格、颜色、适用空间等字段而且库存变动频繁订单流程也偏长。为了后期扩展和维护方便我最终确认的技术栈是 Django Django REST Framework 作为后端 APIVue3 Element Plus Pinia 作为前端Redis 处理缓存和部分临时数据MySQL 存储核心业务数据。接下来我按模块把整个实现过程逐步拆解。1. 项目整体设计为什么选 Python Vue3 这套组合1.1 需求拆解与技术选型先别急着写代码做商城系统最忌讳一上来就建工程。我当时先跟实际业务方确认了三个核心问题用户怎么买东西、运营怎么管理商品、订单从哪里来到哪里去。把这几个问题理清楚后整个系统的功能边界就自然浮出水面了。技术选型方面后端我几乎没有犹豫就选了 Python 生态。原因很实在Python 在电商类应用里的开发效率确实高尤其是 Django 自带 ORM、Admin 后台、认证体系和 Migrations一套组合拳能省掉大量重复工作。Django REST Framework 又提供了现成的序列化器、视图集和路由接口层可以按 RESTful 风格直接铺开。对于家具商城这种业务逻辑相对常规的系统这套组合比手写 SQL 和自研框架要稳得多。前端用 Vue3 也不是跟风。Vue3 的组合式 API 在管理后台这种表单密集、状态共享频繁的场景下非常好用配合 Composition API Pinia代码组织能力明显比 Vue2 时代强。再加上 Element Plus 的组件库后台管理页面基本就是“搭积木”。前端选型对照可以参考下面这个表格技术方案优势劣势适用场景传统后端模板JSP/Thymeleaf开发快、SEO好前后端耦合严重前后台复用困难纯展示型官网或小规模内部系统Django Vue2生态成熟案例多Vue2 已停止维护组合式 API 支持弱旧项目维护或团队技术栈老旧Django Vue3前后端彻底分离组合式 API Pinia 状态管理清晰配套资料相对较少需要理解跨域与鉴权中大型商城、管理后台、需要长期迭代的项目Node.js Vue3全栈语言统一后端生态不如 Python 成熟重业务项目开发效率受影响前端团队主导的轻量项目实际开发时你还会遇到“到底用 Django 还是 Flask”的疑问。我个人的结论是如果项目里有用户体系、ORM、后台管理、多模块权限这些需求优先 Django。Flask 更适合做微服务和纯 API 薄壳商城这种业务密集型的系统Django 的“全家桶”能让你少操很多心。1.2 系统模块划分与功能蓝图系统我拆成了用户端和管理端两个大面。用户端面向 C 端买家要保证购物体验流畅管理端面向运营和客服要数据清晰、操作高效。用户端底子是这样的首页家具分类导航、轮播图、热门商品推荐、新品上架。商品列表页按分类筛选、按风格/材质筛选、价格排序、分页。商品详情页多图展示、规格选择颜色、尺寸、库存显示、加入购物车。购物车勾选商品、修改数量、删除、批量结算。订单中心创建订单、支付模拟、查看订单状态、取消订单、确认收货。个人中心登录注册、收货地址管理、订单列表。管理端则对应着运营需要分类管理一级/二级分类树状管理。商品管理商品基本信息、SKU 规格、库存、上下架。订单管理订单查询、发货、取消订单、退款处理。用户管理用户列表、状态禁用/启用。数据看板今日订单量、销售额、热销商品 Top10。模块清单看起来不复杂但真正实现时每个模块都能挖出一堆细节。尤其是商品和订单这两个是整栋楼的地基后面我会重点展开。2. 后端设计与核心实现2.1 数据库建模让家具商品的属性不再无处安放家具商品的属性跟普通商品不同单纯的一张“商品表”根本放不下。比如一张沙发它的颜色有米白、灰色、墨绿材质有头层牛皮、布艺尺寸还有单人位和三人位。这种多规格、多属性的数据用“商品表 SKU 表”是最合理的方案。核心数据表我在项目里是这样设计的数据表关键字段说明auth_user用户表用户名、密码哈希、手机号、状态Django 自带扩展手机号字段category分类表parent_id、名称、排序、图标支持两级分类product商品表名称、描述、封面图、商品状态、销量基本信息不承载规格product_skuSKU表product_id、规格JSON、价格、库存、SKU编码同一商品下多种规格cart_item购物车表user_id、sku_id、数量、选中状态用户维度存储order订单表order_no、user_id、总金额、状态、收货信息订单头表order_item订单明细表order_id、sku_id、商品名、单价、数量下单时的快照数据这里有个经验值得单独说订单明细一定要做“商品快照”不能直接关联当时的商品表。因为商品的价格、名称、图片后续是会改的如果订单明细直接关联商品表等用户下单三个月后再看订单可能名字和价格都变了。我在 order_item 里冗余了商品名、主图、下单单价、规格描述这样每一笔订单都是历史快照售后和对账都不会乱。Django 模型里SKU 表的规格字段我使用了 JSONField存的是类似{颜色: 灰色, 尺寸: 三人位}这种结构。JSONField 在 Django 3.1 以上是原生支持MySQL 底层对应 JSON 类型查询时也支持按 key 做索引。不过要注意不要用 JSON 字段去做复杂联表查询它适合“存储 读出”不适合做筛选条件。筛选规格时我另外建了 sku_spec_value 的映射表这个是后话。商品和 SKU 模型的精简代码如下# apps/product/models.py from django.db import models class Product(models.Model): STATUS_CHOICES ( (on, 上架), (off, 下架), ) name models.CharField(max_length128, verbose_name商品名称) subtitle models.CharField(max_length255, blankTrue, verbose_name副标题) cover models.CharField(max_length255, verbose_name主图URL) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name分类) status models.CharField(max_length8, choicesSTATUS_CHOICES, defaulton) sales models.IntegerField(default0, verbose_name销量) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class ProductSku(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameskus) sku_code models.CharField(max_length64, uniqueTrue, verbose_nameSKU编码) spec models.JSONField(defaultdict, verbose_name规格JSON) price models.DecimalField(max_digits10, decimal_places2, verbose_name售价) stock models.IntegerField(default0, verbose_name库存) # 原价、图片、重量等可根据业务继续扩展写模型时最容易忽略的就是索引。商品列表页按分类、状态、销量排序这些字段必须加db_indexTrue否则数据量到几百条时感觉不到等商品超过几万条一次列表查询可能就是几百毫秒。别小看这个商城系统的性能问题绝大多数发生在数据库层。2.2 接口设计RESTful 风格与 JWT 权限控制后端接口我全部走 RESTful 风格用 Django REST Framework 的 ViewSet 和 Router 来组织。URL 设计大概是这样请求路径请求方法功能/api/products/categories/GET获取分类树/api/products/GET商品列表支持分类、风格、价格排序/api/products/{id}/GET商品详情含SKU列表/api/cart/GET/POST/PATCH/DELETE购物车查看/加购/改数量/删除/api/orders/GET/POST订单列表/提交订单/api/user/login/POST用户登录/api/user/register/POST用户注册权限控制是商城项目里绕不开的话题。家具商城涉及用户信息、订单、售后不可能让所有接口裸奔。我用了 JWTJSON Web Token做用户认证后端在登录接口签发 token前端每次请求把 token 放到请求头Authorization: Bearer token里DRF 通过authentication_classes统一解析。Django REST Framework 里配置 JWT 认证只需要在settings.py里加上默认认证类# config/settings.py REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), DEFAULT_PERMISSION_CLASSES: ( rest_framework.permissions.IsAuthenticatedOrReadOnly, ), }这里要特别提醒一个实际坑IsAuthenticatedOrReadOnly意味着只有 GET 请求是公开的写操作POST/PATCH/DELETE都需要用户登录。而像商品列表这类公开接口前端如果带了一个过期的 tokenDRF 会返回 401导致商品列表在用户 Session 过期后直接不可见。解决办法是前端 Axios 拦截器里统一处理 401比如跳去重新登录后端则把不需要认证的接口的权限改成AllowAny对应设置好。2.3 核心业务逻辑购物车合并与库存扣减购物车和库存扣减是整个商城最容易出 bug 的地方这里我单独展开。购物车可以分成“未登录状态”和“已登录状态”。早期我做的时候直接让未登录用户不能加购物车结果用户流失严重。后来改成前端用本地存储保存临时购物车用户登录后自动合并到服务端。这样体验好了但实现时要注意合并策略同一 SKU本地临时购物车数量和后端已存在的数量做累加。同一 SKU如果后端库存已经不足则取最大可购数量。合并完成后清空本地购物车避免前端重复渲染。合并逻辑用 Python 写出来类似def merge_cart(user, temp_cart_items): for item in temp_cart_items: sku_id item[sku_id] qty item[count] # 限制不能超过库存 sku ProductSku.objects.select_for_update().get(idsku_id) qty min(qty, sku.stock) cart_item, created CartItem.objects.get_or_create( useruser, skusku, defaults{count: 0} ) cart_item.count min(cart_item.count qty, sku.stock) cart_item.save()库存扣减这个环节我踩过大坑。最初我的下单逻辑是“先查库存是否足够够就直接减”结果上线第二天就出现两个用户同时买同一款沙发数据库里库存变成了负数。原因很简单两个请求同时通过查询判断库存都大于等于购买数量然后各自扣减没有事务隔离就会导致超卖。后来我把扣减逻辑改成了“条件更新 事务”from django.db import transaction def deduct_stock(sku_id, quantity): with transaction.atomic(): # select_for_update 会锁住这一行直到事务结束 sku ProductSku.objects.select_for_update().get(idsku_id) if sku.stock quantity: raise ValueError(库存不足) sku.stock - quantity sku.save()select_for_update()是 MySQL InnoDB 下的行级锁在同一事务里对这条记录加锁其他请求只能等当前事务提交或回滚后才能继续操作。这样能彻底避免并发扣减导致的超卖。当然如果量特别大还可以引入 Redis 预扣库存方案但家具商城这种高单价低频次场景数据库行锁已经足够没必要一开始就把架构搞得特别复杂。订单生成的流程我建议按下面这个顺序走每一步都要独立校验校验购物车中选中的商品是否存在、是否上架。再次计算总金额不能信任前端传过来的总金额。扣减库存并生成订单头和订单明细。清空对应购物车项。返回订单号前端跳转到支付页。最后一步的“前端传总金额不信任”特别重要。做过电商的都懂前端传价格很容易被篡改后端必须以数据库里的 SKU 单价为准重新算一遍。你可以在下单接口里直接根据购物车记录重新聚合金额而不是用请求体里的 total_price。3. 前端 Vue3 实战要点3.1 组合式 API 与 reactive告别 Option API 的“散装”状态前端这块我一开始就决定全面拥抱 Vue3 的组合式 API。刚开始有同事问组合式 API 和原来的 Option API 到底差在哪我用一个购物车状态管理的例子说明。Options API 时代代码被强制分在data、computed、methods、watch这些区块里。一个稍微复杂点的页面可能有十几个方法和七八个数据字段想看懂一段业务逻辑得上下跳来跳去。组合式 API 允许你按“功能逻辑”而不是“选项类型”来组织代码同一段业务逻辑的响应式变量、计算属性、方法可以放在同一个setup函数块里甚至抽到独立的 composable 文件。在商城项目里用户登录状态和购物车状态是全局共享的我用 Pinia 来管理。reactive在 store 种的使用频率非常高比如购物车的列表数据我用reactive包裹了一个数组// stores/cart.js import { defineStore } from pinia import { reactive, computed } from vue export const useCartStore defineStore(cart, () { const state reactive({ items: [], selectedIds: [], }) const selectedCount computed(() state.items .filter((item) state.selectedIds.includes(item.skuId)) .reduce((sum, item) sum item.count, 0) ) function addItem(sku) { const target state.items.find((item) item.skuId sku.skuId) if (target) { target.count 1 } else { state.items.push({ ...sku, count: 1 }) } } return { state, selectedCount, addItem } })这里有个细节reactive只能直接处理对象和数组如果你把reactive包裹后的state通过函数返回在模板中使用时一定不要重新赋值整个state.items否则会丢掉响应性。比如有人会在操作购物车时写state.items response.data这样是错误的reactive对象中已有属性可以被替换但整个items引用被替换后模板可能不会按预期更新。正确做法是先清空数组再push或者干脆用ref。3.2 路由守卫与 Pinia用户登录状态怎么管理用户登录状态是商城系统的门面。前端的 Vue Router 需要做路由守卫页面跳转前检查token是否存在、是否需要登录。我的实现很简单在路由 meta 里标记requiresAuth: true然后在全局前置守卫里做拦截。// router/index.js router.beforeEach(async (to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) return } next() })登录成功后token 存在哪里我选择存 localStorage。这样刷新页面不会丢登录态。但要注意安全性token 不要直接放在 Cookie 里如果没有配合 HttpOnly 和 Secure容易被脚本读取。存 localStorage 也有风险所以 token 有效期要合理设置我这里是 12 小时过期后前端 401 拦截器会自动清除本地 token 并跳转登录页。Pinia 管理用户信息的价值在于一旦用户信息变化比如修改昵称、头像所有页面都能响应。我在userStore里保存了用户信息和 token登录时一次性写入路由守卫里如果已经登录就不需要再重复请求用户信息接口减少网络开销。3.3 Element Plus 搭建后台管理页面后台管理页面是整个项目中工作量占比最高的部分但是有 Element Plus 在效率高了很多。商品管理页我用了el-table展示列表行内加编辑和删除按钮弹窗用el-dialog嵌套el-form做新增和编辑。上传商品主图用el-upload组件前端拿到上传成功后的图片 URL 再回填表单。这里有个 Element Plus 版本上的细节在新版本里el-form的model属性和rules校验逻辑和 Vue2 版本基本一致但是如果你使用script setup语法表单校验实例需要ref来获取。很多人在这里卡住代码长这样script setup import { ref, reactive } from vue const formRef ref(null) const formData reactive({ name: , price: 0, stock: 0, spec: {}, }) const rules { name: [{ required: true, message: 请输入商品名称, trigger: blur }], } async function submit() { await formRef.value.validate() // 提交接口 } /script template el-form refformRef :modelformData :rulesrules el-form-item label商品名称 propname el-input v-modelformData.name / /el-form-item /el-form /templateVue3 的ref在模板中会自动解包所以refformRef拿到的就是表单实例。但是如果你把formRef放在reactive对象里可能会遇到无法正确获取实例的问题建议直接单独用ref声明。后台管理里图片上传的 URL 拼接也有坑后端返回的图片地址如果是相对路径/media/product/xxx.jpg前端浏览器会把它当作当前域名的地址如果你开发环境是localhost:5173请求会打到前端服务拿不到图片。我当时的做法是在 Axios 响应里对图片地址做统一处理如果以/开头就拼上后端域名前缀。你可以写一个全局方法formatImageUrl(url)来处理。4. 联调部署与常见问题排查4.1 前后端联调中的跨域与代理问题前后端分离项目跨域问题几乎必踩。我在开发环境用的是 Vite 的 proxy 代理前端访问/api路径时自动转发到 Django 后端的8000端口这样可以规避一部分 CORS 问题。Vite 配置文件里的 proxy 设置// vite.config.js export default { server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, }, }, }, }开发环境代理配好之后前端请求路径要统一带上/api前缀后端 URL 路由也要保证是/api/...。如果后端本身已经写了/api/products那前端直接写/api/products即可代理会把整个/api/products转发到后端。注意如果一个地址既被 Vite 代理又被 Django 路由匹配容易搞混建议前后端保持一致的 URL 前缀语义。如果是服务器上去掉代理、直接用 Nginx 部署的话跨域就变成了 Nginx 反向代理配置的问题。我在 Nginx 里做了一个简单转发server { listen 80; server_name yourdomain.com; location /api { proxy_pass http://127.0.0.1:8000/api; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static { alias /path/to/django/static/; } location / { root /path/to/vue/dist; try_files $uri $uri/ /index.html; } }这个配置里前端路由的 history 模式也处理了try_files $uri $uri/ /index.html;让 Vue Router 的页面刷新时不至于 404。4.2 数据库与并发场景遇到的坑除了前面说到的库存超卖问题数据库层面还有几个值得记录的坑。第一个是 MySQL 的事务隔离级别。Django 默认使用已提交读级别这个级别下select_for_update依然有效但如果在查询库存之前先做了其他读操作可能会出现幻读。所以扣库存的代码最好放在事务开头并且只做这一件事的查询。我实测下来把扣减逻辑放到事务最前面最稳。第二个是 Django 的 ORM 查询不走索引。比如商品列表页按分类筛选如果分类字段没有索引且分类下面有几千个商品查询就会全表扫描。早期测试数据量小看不出来但商品数据到两三万条后接口响应时间从几十毫秒飙升到两秒以上。后来我给所有外键和状态字段加了db_indexTrue并在数据库层面用EXPLAIN验证查询计划问题才彻底解决。第三个是购物车接口并发问题。用户狂点“加入购物车”按钮导致同一 SKU 在购物车表中出现多条记录。解决办法是用get_or_create保证同一用户同一 SKU 只有一条购物车记录然后在数据库层面给(user_id, sku_id)加唯一约束。4.3 部署与 Python 环境细节部署阶段对新手来说最容易卡在环境上。如果你是在自己的电脑上跑这套系统我建议先确认 Python 环境是不是干净且版本匹配的。我开发时用的是 Python 3.11Django 4.2Vue3 的构建工具要求 Node 18 以上这几个版本之间没有兼容冲突。Windows 上装 Python 时要特别注意勾选Add Python to PATH选项否则装完在命令行输入python启动不了。很多朋友报“python 不是内部或外部命令”基本都是这个原因。安装依赖时推荐用虚拟环境Django 项目最怕全局环境里的包版本互相干扰python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txtVue 前端构建时如果npm install出现 node-sass 相关的报错大概率是 Node 版本和新版 Sass 不兼容建议直接用npm install重新安装依赖或者升级到 sass 对应的版本。构建命令用npm run build构建产物在dist目录里上传到服务器后放到 Nginx 配置的 root 目录就行。生产环境还有一个容易忽略的问题Django 的SECRET_KEY、数据库密码这些敏感信息不要硬编码在代码里。我习惯用环境变量读取配合.env文件管理然后.env文件加入.gitignore。这虽然不是功能问题但关系到项目后续维护的安全底线。5. 项目扩展方向与个人心得5.1 功能还能怎么扩展这套家具商城跑通基础流程后可以按业务需要继续扩展。常见的方向有三个一是接入真实支付。目前我做的支付是模拟流程要真接入的话可以在订单创建后生成支付二维码回调接口处理支付结果。要注意支付回调必须是服务端到服务端的不能信任前端通知。二是加入推荐系统。家具购买决策时间长用户访问行为数据足够后可以做一个简单的协同过滤推荐在首页和个人中心展示相关商品。用 Python 机器学习库做离线推荐再缓存 Redis前端只需要一个推荐接口。三是多端适配。目前前端是 PC 为主的商城和管理后台家具商城还有很多线下门店导购场景后续可以基于 Vue3 写一个移动端 H5 或者小程序端后端接口复用率会比较高。5.2 我最想分享的几个实操体会第一点商城系统的开发顺序非常重要。建议先打通“商品列表 → 商品详情 → 加购物车 → 提交订单 → 订单列表”这条核心链路再做后台管理。先能跑通流程后面再优化体验和增加功能才有意义。如果一上来就啃后台管理容易把精力耗在边角功能上。第二点写后端接口时响应结构的统一极其重要。我封装了一个通用的响应格式{ code: 0, message: success, data: {...} }。所有接口都走这个壳子前端 Axios 响应拦截器统一解构遇到非 0 的 code 统一弹错误提示。没有统一响应格式的话前后端联调的时候光对齐字段就能把人搞疯。第三点不要过度设计。像 Redis 缓存、消息队列、分布式锁这些一开始根本不需要上。等你的商城真的到了日均上万单、并发几百再考虑也来得及。我见过太多项目开头就引入一堆中间件结果部署复杂、排障困难最后连核心业务都跑不顺。先用最简单的方案跑通发现问题再升级这才是个人项目最合适的节奏。这套家具商城系统最终交付的时候核心功能一个没落下前端和后台加起来大概四十多个接口整个项目维护起来也比较轻松。如果你也在用 Python 和 Vue3 搭类似的系统希望这篇记录能帮你绕开我已经踩过的坑少走几步弯路。