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 做一层缓存,减少数据库压力。

缺点:

  • 实现复杂,需要合并逻辑。

二、我们项目的技术选型

本着“先跑通核心流程,再逐步优化”的原则,我们选择 方案一:数据库存储(登录用户专用),原因如下:

  1. 教学清晰:数据库方式最直观,适合初学者理解购物车的 CRUD 操作。

  2. 已有基础:第 2 篇已定义了 CartItem 模型,今天只需回顾和优化。

  3. 后续可扩展:第 24 篇会用 Redis 给商品做缓存,届时可以自然地将购物车也迁移到 Redis 缓存层(方案五)。现在我们先把数据库版跑通。

  4. 未登录用户:我们强制要求登录后才能操作购物车,通过 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 优化。目前先保持简单,后面视图层会主动处理校验。


五、购物车业务规则梳理

在写视图之前,我们先明确购物车的核心业务规则:

  1. 添加商品

    • 同一 SKU 已存在 → 数量累加(不超过库存);

    • 不存在 → 创建新记录;

    • 数量默认为 1,可从前端传入。

  2. 修改数量

    • 增加或减少,但不能小于 1,不能超过库存;

    • 如果数量减为 0 → 删除记录(或保持最小为 1,视产品设计,我们采用最少 1,删除有单独接口)。

  3. 勾选与取消勾选

    • 用户可勾选/取消勾选单个商品;

    • 可选“全选/取消全选”。

  4. 删除

    • 支持单个删除和批量删除(勾选后删除选中项)。
  5. 结算条件

    • 必须有至少一个勾选的商品;

    • 所有勾选商品必须库存充足;

    • 用户必须至少有一个收货地址。


六、商品详情页“加入购物车”入口准备

第 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策士,未经授权禁止转载。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐