接到一个茶叶销售系统的开发需求时,我的第一反应是:这不就是个电商系统嘛。可等到把需求文档一条条过完,才发现茶叶这类目背后藏着一堆商品逻辑——品类分级、产地、工艺、年份、批次口感,甚至同一款茶换个山头味道就完全不一样。这些细节直接决定了你能不能照搬一套通用商城模板来应付差事。
说实话,这种业务不复杂,但很琐碎。后端的商品建模、订单状态机、库存扣减逻辑,前端的筛选导航、规格联动、购物车交互,每一步都需要踩过一遍才会觉得顺畅。我这次用的是 Python 系技术栈:后端 Django 框架,前端 Vue,日常开发用 PyCharm。整个项目做下来,最大的感受就是:选型没踩坑,后面就顺了。
如果你想做一个能在课堂上演示、或者能直接内部使用的中小型电商平台,这篇内容基本可以当作一份实操地图。我会从需求梳理、技术选型、数据库设计、前后端联调,一直讲到开发阶段那些坑,尽量把每一个关键决策背后的“为什么”也说清楚。
1. 项目到底要做什么:从需求梳理到技术画像
1.1 茶叶电商的特殊性:为什么不能套普通商城模板
很多人一上来就建商品表,字段就是 title、price、stock、image,然后转头开始写接口。这种做法在茶叶项目里基本会返工。
普通商品可能只需要名称、价格、库存、图片这几个字段就够用了,但茶叶商品在用户决策链路里面对的信息远比这多。用户在看一款茶叶时,除了价格,还关心产地在哪里、采摘季节是明前还是雨前、等级是特级还是二级、工艺是炒青还是烘青、存放年份是哪一年的。这些属性不是靠一个“描述”文本框就能塞进去的——它们需要被结构化,因为用户会拿它们来筛选和比较。
举个例子,列表页通常需要按“品类 + 产地 + 等级 + 年份”做多条件筛选。如果这些属性全放在一个大的 description 字段里,那筛选就只能靠数据库 LIKE 查询,数据量一旦上来,查询会慢得让人怀疑人生。更麻烦的是,商品详情页要展示“规格选择”,比如同一款铁观音分成 250g 装和 500g 装,价格、库存都不同。这种结构用单表模型很难优雅地表达。
所以我在数据库设计层面的第一件事,就是把商品表拆成“基础信息 + 规格属性 + 详情说明”三层结构,而不是平铺成一个大宽表。这算是茶叶电商和其他标品电商最大的差异点。
1.2 技术选型:Django vs Flask + Vue 的取舍
项目标题里同时出现了 Django 和 Flask,这两个框架确实都能做,但最终我选了 Django,原因不在框架本身的好坏,而在于这个项目需要的“原生能力”。
Django 自带 Admin 后台、ORM、迁移机制、用户认证、CSRF 防护,这些对于一个电商后台来说全是刚需。茶叶商品需要频繁维护 SKU 和库存,有一个能直接录入、编辑数据的后台能省掉大量手动写 CRUD 的时间。Django Admin 免费且开箱即用,配上自定义筛选器,甚至可以选择产地、品类字段来过滤商品,这在日常维护中非常实用。
如果用 Flask:路由、模板、会话都需要自己选插件,ORM 可以选择 SQLAlchemy 来补,Admin 界面则需要自己搭或者用第三方扩展。Flask 最大的优势是灵活、启动快、代码量少,适合做 API 层的轻量服务。但当一个项目里同时包含后台管理、商品管理、订单流程、用户体系的时候,Flask 的“自由”反而会成为约束——你需要花很多精力去决定各种组件的选型和集成方式,而这些事情 Django 已经替你安排好了。
前端为什么选 Vue:Vue 的渐进式框架特性很适合电商类应用。商品列表页的搜索、筛选、排序这些交互可以拆成独立组件,购物车和用户状态放在全局状态管理里,整个开发体验对中小型团队很友好。Vue 3 搭配 Vite 的开发服务器启动速度很快,加上 Element Plus 这样的成熟组件库,做后台管理页面几乎不用自己写 CSS。
最终选型定下来:Django + Django REST Framework 提供 API,Vue 3 + Element Plus 做前端,PyCharm 作为主力 IDE。这套组合的好处是 Django 把后端基础设施全部备齐,Vue 承接了全部前端交互,两种技术各司其职,没有哪个环节需要我来硬凑。
1.3 PyCharm 项目环境配置要点
开发工具这一块,PyCharm 最核心的价值是调试体验。电商项目里最烦的就是定位某个订单状态流转的问题,而 PyCharm 的断点调试可以一步步看到 ORM 查询返回了什么、序列化器在哪一步丢了字段,比靠 print 打日志高效太多。
新建项目时有几个配置建议:虚拟环境隔离依赖,Python 用 3.9 以上版本,Django 建议直接用当前稳定版本。requirements.txt 里至少包含这几个包:
django djangorestframework django-cors-headers Pillow python-dotenv djangorestframework-simplejwt然后建议在 PyCharm 的 Run/Debug Configurations 里把环境变量配好,比如数据库连接字符串、SECRET_KEY、DEBUG 开关,不要写死在 settings.py 里。很多人第一次跑项目崩在缺少某个环境变量上,配好之后一劳永逸。
还有一个被忽视的点:统一编码和换行符。Windows 下开发 Python 项目时,注意把文件编码设为 UTF-8,否则注释里的中文在某些场景下会变成乱码,排查起来非常让人头疼。
2. 核心模块拆解与数据库设计
2.1 茶叶商品模型设计
有了业务需求和技术底座,接下来就是数据库设计。这是整个项目最关键的部分,因为它决定了后面所有接口、页面的复杂程度。
我把茶叶商品拆成三张核心表:品类表(Category)、商品表(TeaProduct)、规格库存表(ProductSKU)。
品类表很简单:id、name、parent_id(如果有二级分类)、sort_order。茶叶品类我喜欢维持两级结构,比如“绿茶”下面分“龙井”“碧螺春”,这样前端可以做两级联动筛选。
商品表存的是“一个茶叶商品”的公共信息,这些字段是所有规格共享的:
class TeaProduct(models.Model): name = models.CharField(max_length=200, verbose_name="商品名称") category = models.ForeignKey( Category, on_delete=models.PROTECT, related_name="products" ) origin = models.CharField(max_length=100, blank=True, verbose_name="产地") harvest_year = models.PositiveIntegerField(null=True, blank=True, verbose_name="年份") grade = models.CharField(max_length=50, blank=True, verbose_name="等级") process = models.CharField(max_length=200, blank=True, verbose_name="工艺") description = models.TextField(blank=True, verbose_name="详细介绍") cover_image = models.ImageField(upload_to="products/", blank=True, verbose_name="主图") is_active = models.BooleanField(default=True, verbose_name="是否上架") created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at"] verbose_name = "茶叶商品"规格库存表存价格和库存,比如一款明前绿茶分 250g 装和 500g 装,价格、库存都不同:
class ProductSKU(models.Model): product = models.ForeignKey( TeaProduct, on_delete=models.CASCADE, related_name="skus" ) sku_name = models.CharField(max_length=100, verbose_name="规格名称") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="售价") stock = models.PositiveIntegerField(default=0, verbose_name="库存") is_active = models.BooleanField(default=True, verbose_name="是否启用") create_time = models.DateTimeField(auto_now_add=True)为什么不用一个大宽表?因为如果把“规格名称”“价格”“库存”直接塞到商品表里,会出现大量冗余字段,而且每增加一个规格就要改表结构。拆出 SKU 表后,商品详情页需要展示“可选规格 + 对应价格库存”时,一次查询就能全部拿到。
有个细节:很多课程设计里会用 JSONField 来存规格,比如 specs = {"克重": "250g"}。在小项目里这确实省事,但如果你后面要做“按规格过滤”“对比库存”这类操作,JSONField 的查询效率和可维护性都远不如独立表。我建议即使再小的项目也至少把 SKU 拆出来,这是我在几个真实项目里反复验证过的经验。
2.2 用户认证与订单流程
用户部分,Django 自带的 User 模型其实够用。如果要存手机号、收货地址,可以定义一个 Profile 模型通过 OneToOne 关联到 User,或者直接扩展 AbstractUser。我偏好扩展 AbstractUser,因为这样后台管理界面更加统一,不需要额外处理关联关系。
收货地址单独建表,核心字段就是收货人、电话、省市区、详细地址、默认标志位。和电商业务相关的表里,order 和 order_item 是必有的:
订单主表存订单号、用户、总金额、收货信息快照、状态、创建时间。为什么收货信息要快照?因为用户下单后可能修改了默认地址,但订单不能跟着变,必须把下单那一刻的地址冗余到订单表里。
订单状态是最需要想清楚的部分。我最常用的状态流是这样的:
- PENDING:待支付
- PAID:已支付
- SHIPPED:已发货
- COMPLETED:已完成
- CANCELED:已取消
状态字段用 choices 定义在模型里,并在代码层维护一个状态转移映射表。比如只有 PENDING 状态才能执行支付操作,只有 PAID 状态才能发货。这样可以在接口层做统一校验,避免状态任意跳转。
订单号生成建议:不要用自增 ID 暴露订单量。我常用的是 yyyyMMddHHmmss + 6 位随机数。这里有个小计算:如果不考虑极端并发,这种订单号在单日内有百万级空间,对中小型系统完全够用。
2.3 购物车与库存联动
购物车在前后端分离项目里有两种实现方式:存本地(localStorage)或存服务端。
如果用户未登录时也要能加购物车,前端 localStorage 更友好;用户登录后,建议把购物车同步到服务端。但这里有个取舍——同步逻辑会带来不少代码量。对于中小型项目,我实际的选择是:服务端购物车,强制登录后才能加入购物车。虽然会损失一部分未登录用户的购物体验,但代码简单、数据一致性强,对演示或内网使用的场景完全足够。
库存扣减的时机我建议“下单时冻结”。也就是创建订单的同时,把对应 SKU 的库存扣掉,并记录到一个“冻结库存”字段。当订单取消或超时未支付时,释放冻结库存。如果等支付后再扣库存,可能出现用户下了单但付钱后已经没有货的问题。
防超卖的核心做法:在扣减库存的查询语句里加条件,保证 stock >= 需要扣减的数量,或者使用数据库行锁。用 Django 的 select_for_update() 可以很方便地锁定 SKU 行:
from django.db import transaction with transaction.atomic(): sku = ProductSKU.objects.select_for_update().get(id=sku_id) if sku.stock < quantity: raise ValidationError("库存不足") sku.stock -= quantity sku.save()这段代码看起来简单,但如果没有 select_for_update(),高并发下两个请求同时读到 stock=1,就可能都通过校验,导致超卖。
购物车表同样简单,核心字段:user、sku、quantity、checked、created_at。一个用户对同一个 SKU 只能有一条记录,数量累加即可,可以用 unique_together 约束 user 和 sku。
3. 前后端联调实操:构建 Vue 前端与 Django API
3.1 RESTful API 设计实践
后端这边明确建议使用 Django REST Framework(DRF),它帮你完成了序列化、认证、权限管理、分页,省下大量重复代码。
API 设计遵循 RESTful 风格,接口清单如下:
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | /api/products/ | 商品列表,支持关键词、品类、产地筛选和排序 |
| GET | /api/products/{id}/ | 商品详情,含 SKU 列表 |
| GET | /api/categories/ | 品类树 |
| POST | /api/cart/ | 加入购物车 |
| GET | /api/cart/ | 获取购物车 |
| PATCH | /api/cart/{id}/ | 修改购物车数量 |
| DELETE | /api/cart/{id}/ | 删除购物车项 |
| POST | /api/orders/ | 创建订单 |
| GET | /api/orders/ | 我的订单列表 |
| GET | /api/orders/{id}/ | 订单详情 |
序列化器是 DRF 里最核心的部分。以商品详情为例,嵌套序列化 SKU 列表时,用 ModelSerializer 配合 related_name 就能一条请求返回完整数据。需要注意两点:一是写了嵌套序列化器却忘了设置 related_name,导致序列化器不知道去哪取数据;二是在列表页丢掉了 select_related 和 prefetch_related,导致 N+1 查询,页面越翻越慢。
3.2 Vue 前端核心实现
前端用 Vue 3 + Vite 初始化项目。组件划分上,我会把商品列表页拆成商品卡片、筛选侧栏、搜索栏、分页条。购物车页面拆成购物车列表、数量增减按钮、结算栏。
状态管理用 Pinia,管理购物车数量、用户 token、当前筛选条件。以购物车为例,每次加入购物车后 Pinia 里的 state 更新,同时调用后端接口同步,两边保持一致。
Vue 组件中比较核心的一段是加入购物车的方法:
async function addToCart(skuId, quantity = 1) { const { data } = await axios.post('/api/cart/', { sku: skuId, quantity: quantity, }) cartStore.setCart(data) ElMessage.success('已加入购物车') }搜索和筛选的处理:输入框加 300ms 的防抖,避免每敲一个字符就触发一次请求。筛选条件变化时重置分页参数。这些看起来是小事,但直接影响用户体验和接口压力。
还有一个容易忽略的点:列表页要展示的“规格选择”在前端其实是把一组 SKU 展示成选项卡,选中不同规格时,价格和库存跟着变。这个交互的关键在于用 sku_id 作为唯一标识,而不是拿规格名称字符串去匹配,否则一旦名称里有空格或全角字符差异,就会出现选中后价格不刷新的怪毛病。
3.3 联调阶段注意事项
前后端分离项目最常见的第一个报错就是跨域。Django 后端默认不允许跨域请求,需要安装 django-cors-headers,并在 settings.py 配置 CORS_ALLOWED_ORIGINS。开发环境图方便可以直接 CORS_ALLOW_ALL_ORIGINS = True,但生产环境一定要指定域名白名单,否则任何网站都能请求你的接口,存在安全问题。
另一个是 token 传递。如果用 JWT,前端 axios 请求拦截器里可以把 token 加到 Authorization 头:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config })后端 DRF 里配置 DefaultAuthenticationClasses 为 JWT 认证,这样所有需要登录的接口都会自动校验 token,不用每个视图重复写。
本地开发时,我习惯在 Vite config 里配置 proxy,把 /api 前缀请求转发到本地的 8000 端口,这样浏览器里不会出现跨域问题,也不用每次修改 CORS 配置:
// vite.config.js server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, } } }这里有个细节:开发时的环境变量和最终部署时的 API 地址往往不同。我习惯把接口基础路径统一放在一个 request.js 模块里管理,页面组件不直接写 axios 地址,这样上线时只改一个文件就行。
4. 踩坑记录与排查技巧
4.1 高频报错速查表
这里列一些我实际开发中遇到的高频报错,方便你直接对照排查:
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
| 前端请求接口 403 | CSRF 或认证未通过 | DRF 视图区分 AllowAny / IsAuthenticated,JWT 检查 token |
| 商品列表打开后加载慢 | 未用 prefetch_related | 给外键和多对多字段批量预取,避免逐条查询 |
| 图片上传后 404 | MEDIA_URL 没配置或开发环境未托管 | settings 配置 MEDIA_ROOT/MEDIA_URL,urls 里加 static 处理 |
| 购物车点击加入无反应 | 请求未携带 token | 检查 axios 拦截器,确认登录后 token 已存 localStorage |
| 序列化后没有字段 | 忘记设置 related_name 或字段名不一致 | 打印 serializer.data 对比模型字段 |
| 数据中文字段乱码 | 编码配置问题 | 检查连接参数和文件编码,统一 UTF-8 |
4.2 图片上传与静态资源管理的坑
这个必须单独拿出来说。Django 的 ImageField 字段只是声明了“这是一个图片文件”,真正让图片能被上传和访问,需要正确设置 MEDIA_ROOT 和 MEDIA_URL。
开发环境下可以使用 Django 自带的 static 视图处理:
from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ... 你的接口路由 ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)生产环境里,通常会由 Nginx 这类 Web 服务器直接托管 media 目录。如果你部署到服务器后发现商品图片全部无法访问,大概率是 Nginx 配置里 alias 路径写错了,和 Django 代码没关系。
还有一个细节:前端上传图片时,需要把 Content-Type 设置为 multipart/form-data,并携带文件对象。很多人用 axios 上传时习惯性设置 JSON 请求头,导致后端解析不到文件,这个问题很隐蔽,因为接口并不会报 500,而是返回类似“此字段为必填项”的校验错误。
4.3 从演示项目到真实上线的距离
如果你只是做一个课程设计或者内部演示,到这里其实已经可以了。但如果是真正要对外商用,有几个问题需要提前想清楚。
第一,支付方式。对接真实支付接口需要商户资质,对于个人开发者最现实的选择是用沙箱环境模拟支付流程。这样订单状态机里的“已支付”分支至少能被完整走通,切换真实支付时的改动也集中在支付回调这一块。
第二,密码和用户数据安全。Django 自带 PBKDF2 加密,默认配置不要盲目关掉。不要自己写加密算法,不要用明文存储密码。另外 SECRET_KEY 一定不要提交到 Git 仓库,泄露之后攻击者可以伪造会话。
第三,性能优化。初期数据量小,查询简单不会暴露问题。当商品数据量达到几百条以上、关键词搜索和筛选查询频繁时,要给 name、category、origin 这些字段建立索引,并考虑加一层 Redis 缓存。列表页的分页参数也要好好调,不要一次全量返回。
后续扩展方向也比较明确:增加商品规格多图、接入物流查询、做用户积分体系、开发小程序端。由于前后端分离的架构已经定好,这些扩展基本就是加模块的事,数据库表和 API 接口都能平滑演进。
最后说一点个人体会。这类电商项目真正值钱的不是“跑通”,而是你在过程中建立的取舍意识:为什么把规格拆成表、为什么下单时冻结库存、为什么 API 要遵循 RESTful 而不是随手写一批接口。每一步选择背后都有业务逻辑在推动。
顺手再分享一个小细节:开发时给每个 model 加上 verbose_name,给每个 field 也写上中文 verbose_name。看起来只是锦上添花,但当你计划靠 Django Admin 维护数据、或者几天后回头看自己的项目时,会发现这些中文标注帮你节省大量回忆时间。
茶叶销售系统的业务不大,但麻雀虽小五脏俱全。把它认真做完,你基本就掌握了前后端分离电商项目的全部主干知识点——这些能力换个品类、换个业务场景依然能复用。