Django 从 0 到 1 打造完整电商平台:购物车实现方式分析与模型设计
摘要 本文详细分析了电商购物车的5种技术实现方案,包括数据库存储、Session/Cookie存储、Redis缓存、前端LocalStorage和混合方案,并对比了各自的优缺点。基于教学清晰度和扩展性考虑,作者选择数据库存储方案作为项目实现方式。文章回顾并优化了购物车模型设计,增加了库存校验逻辑,梳理了核心业务规则,为后续开发做好准备。最后激活了商品详情页的"加入购物车"按钮,并添加了登录判断功能
IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。
大家好,我是IT策士。商品模块已经完整了——用户能浏览、搜索、排序、查看详情,购物决策所需的全部信息都已就位。从今天起,我们正式踏入电商最核心的转化环节:购物车。
购物车看似简单,实际上实现方式多种多样,不同的业务场景需要不同的技术选型。本篇我会先带你分析业界常见的购物车方案,然后确定我们项目的技术路线,最后完善模型设计,为明天的增删改查打好基础。
一、购物车实现方式全景分析
实现购物车,常见的有以下几种方案:
方案一:数据库存储(登录用户专用)
购物车数据直接存数据库,表结构与 CartItem 类似,user + sku 唯一确定一条记录。
优点:
-
数据持久化,用户换设备登录后购物车还在;
-
方便数据分析,可以统计用户购物车商品的偏好;
-
适合需要长期保留购物车的场景(如亚马逊)。
缺点:
-
未登录用户无法使用;
-
每次页面请求都查数据库,高并发下压力大。
方案二:Session / Cookie 存储
购物车数据存储在用户的浏览器 Cookie 或服务端 Session 中。
优点:
-
未登录也能用,降低下单门槛;
-
不需要数据库写入,速度快。
缺点:
-
Cookie 容量有限(4KB),商品多了存不下;
-
Session 依赖服务端,分布式部署需要共享 Session;
-
用户清缓存或换设备,购物车丢失。
方案三:Redis 缓存
用 Redis 的 Hash 结构存储购物车,键为 cart:user_id,field 为 SKU ID,value 为数量等信息。
优点:
-
读写极快,支持高并发;
-
可以设过期时间,自动清理僵尸购物车;
-
数据结构和购物车天然匹配。
缺点:
-
需要额外维护 Redis 集群;
-
数据持久性不如数据库(需开启 RDB/AOF 且仍可能丢数据);
-
未登录用户需要额外处理(如用设备 ID 当 key)。
方案四:前端 LocalStorage
纯前端方案,购物车数据存浏览器 LocalStorage,请求时通过 API 传给后端。
优点:
-
服务端零存储成本;
-
未登录流畅使用。
缺点:
-
换设备丢失;
-
用户可篡改数据,安全性差;
-
后端下单时仍需校验价格库存。
方案五:混合方案(数据库 + Redis)
登录用户用数据库存储(持久化),未登录用户用 Cookie/Session(临时),登录后合并购物车。这是京东、淘宝等主流电商的常见做法。
优点:
-
兼顾未登录用户的体验和数据持久化;
-
Redis 做一层缓存,减少数据库压力。
缺点:
- 实现复杂,需要合并逻辑。
二、我们项目的技术选型
本着“先跑通核心流程,再逐步优化”的原则,我们选择 方案一:数据库存储(登录用户专用),原因如下:
-
教学清晰:数据库方式最直观,适合初学者理解购物车的 CRUD 操作。
-
已有基础:第 2 篇已定义了
CartItem模型,今天只需回顾和优化。 -
后续可扩展:第 24 篇会用 Redis 给商品做缓存,届时可以自然地将购物车也迁移到 Redis 缓存层(方案五)。现在我们先把数据库版跑通。
-
未登录用户:我们强制要求登录后才能操作购物车,通过
login_required装饰器限制。这是很多电商(如京东)的常规做法——加购前先登录。
设计决策:商品详情页的“加入购物车”按钮,如果用户未登录,点击后跳转登录页,登录后重定向回详情页。
三、购物车模型回顾与优化
第 2 篇我们写的 CartItem 模型在 apps/cart/models.py 中,先回顾一下:
class CartItem(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='cart_items',
verbose_name='所属用户'
)
sku = models.ForeignKey(
'products.SKU',
on_delete=models.CASCADE,
verbose_name='商品 SKU'
)
quantity = models.PositiveIntegerField(
default=1,
verbose_name='购买数量'
)
is_checked = models.BooleanField(
default=True,
verbose_name='是否勾选'
)
create_time = models.DateTimeField(auto_now_add=True, verbose_name='添加时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'tb_cart_item'
verbose_name = '购物车条目'
verbose_name_plural = verbose_name
unique_together = ('user', 'sku')
设计要点解读:
四、模型优化——增加库存校验逻辑
当前模型没有在保存时校验数量是否超过库存。我们需要在视图层做校验,但也可以在模型中加一个简单的方法来方便后续调用。
在 CartItem 模型中添加:
from django.core.exceptions import ValidationError
class CartItem(models.Model):
# ... 字段省略 ...
def clean(self):
"""校验购买数量不超过库存"""
if self.quantity > self.sku.stock:
raise ValidationError(f'商品 “{self.sku.name}” 库存不足(当前库存:{self.sku.stock})')
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
full_clean() 会自动调用 clean() 方法。这样无论是通过表单还是直接调用 save(),都会触发校验。
但这里有个细节:CartItem 保存时 sku 可能还没设置(比如通过 Admin 创建时),所以我们在 clean() 里先判断 self.sku_id 是否存在。优化如下:
def clean(self):
if self.sku_id and hasattr(self, 'sku'):
if self.quantity > self.sku.stock:
raise ValidationError(f'商品 “{self.sku.name}” 库存不足(当前库存:{self.sku.stock})')
不过,Model.full_clean() 在 save() 中调用会增加一次数据库查询(获取 self.sku),生产环境可通过 select_related 优化。目前先保持简单,后面视图层会主动处理校验。
五、购物车业务规则梳理
在写视图之前,我们先明确购物车的核心业务规则:
-
添加商品:
-
同一 SKU 已存在 → 数量累加(不超过库存);
-
不存在 → 创建新记录;
-
数量默认为 1,可从前端传入。
-
-
修改数量:
-
增加或减少,但不能小于 1,不能超过库存;
-
如果数量减为 0 → 删除记录(或保持最小为 1,视产品设计,我们采用最少 1,删除有单独接口)。
-
-
勾选与取消勾选:
-
用户可勾选/取消勾选单个商品;
-
可选“全选/取消全选”。
-
-
删除:
- 支持单个删除和批量删除(勾选后删除选中项)。
-
结算条件:
-
必须有至少一个勾选的商品;
-
所有勾选商品必须库存充足;
-
用户必须至少有一个收货地址。
-
六、商品详情页“加入购物车”入口准备
第 13 篇我们留了“加入购物车”按钮但设为了 disabled。现在我们先在模板中把按钮激活,并加上登录判断。
编辑 apps/products/templates/products/spu_detail.html,修改按钮区域:
<div class="mt-4">
{% if user.is_authenticated %}
<button class="btn btn-primary btn-lg" id="add-to-cart-btn">加入购物车</button>
<button class="btn btn-danger btn-lg ms-2" disabled>立即购买</button>
{% else %}
<a href="{% url 'users:login' %}?next={{ request.path }}" class="btn btn-primary btn-lg">登录后购买</a>
{% endif %}
</div>
此时按钮还没有绑定 JS 事件,我们明天会写 AJAX 提交逻辑。今天先让页面展示正常。
七、购物车模型单元测试(可选但推荐)
为了确保模型方法正确,我们写一个简单的单元测试。在 apps/cart/tests.py 中:
from django.test import TestCase
from django.contrib.auth import get_user_model
from products.models import SPU, SKU, Category
from .models import CartItem
User = get_user_model()
class CartItemModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='testpass')
self.category = Category.objects.create(name='测试分类', level=1)
self.spu = SPU.objects.create(name='测试商品', category=self.category)
self.sku = SKU.objects.create(
spu=self.spu, name='测试SKU', price=100, stock=10, is_active=True
)
def test_create_cart_item(self):
item = CartItem.objects.create(user=self.user, sku=self.sku, quantity=2)
self.assertEqual(item.quantity, 2)
self.assertTrue(item.is_checked)
def test_unique_together(self):
CartItem.objects.create(user=self.user, sku=self.sku, quantity=1)
# 同一用户同一 SKU 不能再创建第二条
with self.assertRaises(Exception):
CartItem.objects.create(user=self.user, sku=self.sku, quantity=3)
def test_quantity_exceeds_stock(self):
item = CartItem(user=self.user, sku=self.sku, quantity=20)
with self.assertRaises(Exception):
item.save()
运行测试:
python manage.py test cart
预期输出:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.123s
OK
Destroying test database for alias 'default'...
测试通过,说明唯一约束和库存校验正常工作。
八、总结与下集预告
今天我们花了大量篇幅做购物车的方案分析和模型固化,这是很重要的“谋定而后动”的环节:
-
分析了 5 种购物车实现方案,了解了各自的适用场景;
-
确定采用数据库存储(登录用户专用),符合教学顺序和项目规模;
-
回顾并优化了
CartItem模型,添加了库存校验方法; -
编写了单元测试,确保模型逻辑正确。
现在,购物车的“设计图纸”已经画好,明天就是真正的施工阶段。第 17 篇,我将带你实现购物车的完整增删改查页面:从商品详情页 AJAX 加入购物车、购物车列表页修改数量/勾选/删除、到全选和合计金额计算,一气呵成。这将是目前交互最复杂的一篇。
想了解更多还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !
本文为《Django 从 0 到 1 打造完整电商平台》系列第 16 篇,作者:IT策士,未经授权禁止转载。
更多推荐

所有评论(0)