Django 从 0 到 1 打造完整电商平台:电商项目需求分析与数据库设计

0 阅读11分钟

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。

电商项目需求分析与数据库设计

各位小伙伴好,我是IT策士。上一节我们搭好了项目骨架,创建了 users、products、cart、orders、payment 五个 app,并且让开发服务器跑了起来。今天我们要做一个项目里 最不能马虎 的环节——需求分析与数据库设计。数据库是整个系统的地基,地基歪了,后面所有的功能都会“塌方”。

接下来我们会先把电商平台的核心业务梳理清楚,然后画出实体关系图,最后动手把 Django 的模型代码写出来并真正建表。


一、电商平台需求分析

1.1 用户角色

一个典型的电商平台至少包含两类角色:

  • 普通用户(买家):浏览商品、加入购物车、下单、支付、查看订单、管理地址。

  • 管理员:通过 Django Admin 管理商品、分类、订单、用户。

本系列主要实现的是 面向买家 的电商系统,管理员的功能会大量依赖 Django Admin 来完成,不会单独开发一套管理后台。

1.2 核心功能模块

拆解成六大模块,正好对应我们创建的五个 app + 一个抽象层(支付虽独立 app,但属于订单模块的延伸):

1.3 业务流程主线

一个完整的购物流程大致如下,这也是我们后面开发的主线:

  1. 用户注册并登录;

  2. 浏览商品列表 / 搜索商品 / 查看详情;

  3. 将中意的 SKU 加入购物车;

  4. 在购物车页面确认商品、数量,点击“去结算”;

  5. 进入确认订单页,选择收货地址,提交订单;

  6. 跳转支付宝沙箱付款;

  7. 支付成功后返回,查看我的订单。

这套流程会把我们所有的 app 串联起来,因此数据库设计必须能承载这些流转的数据。


二、数据库设计——从 E-R 图到数据表

2.1 核心实体梳理

根据需求,我们可以抽象出以下几个核心实体:

  • 用户(User)

  • 收货地址(Address)

  • 商品分类(Category)

  • 商品 SPU(Standard Product Unit)

  • 商品 SKU(Stock Keeping Unit)——实际售卖的单位

  • 商品图片(ProductImage)

  • 购物车条目(CartItem)

  • 订单(Order)

  • 订单商品条目(OrderItem)

  • 支付记录(Payment)

名词解释:SPU 代表“标准化产品单元”,比如 iPhone 15;SKU 代表“库存量单位”,是具体到规格的商品,比如“iPhone 15 128G 午夜色”。一款 SPU 下可以有多个 SKU。

2.2 实体间关系(E-R 图)

因为 Markdown 不能直接画图,我用最直白的“连线描述法”来表示:

[用户] 1 ──── N [收货地址]         (一个用户有多个地址)
[商品分类] 1 ──── N [商品分类]     (自关联,实现无限级分类树)
[商品分类] 1 ──── N [SPU]
[SPU]    1 ──── N [SKU]
[SKU]    1 ──── N [商品图片]
[用户]   1 ──── N [购物车条目]     (一个用户有多个购物车项)
[SKU]    1 ──── N [购物车条目]
[用户]   1 ──── N [订单]
[订单]   1 ──── N [订单商品条目]
[SKU]    1 ──── N [订单商品条目]
[订单]   1 ──── 1 [支付记录]

注意:订单里的收货地址我们会做“快照”处理,不直接外键关联地址表,防止用户修改地址后影响历史订单。


三、配置 AUTH_USER_MODEL(非常重要!)

Django 自带的 User 模型字段有限,而我们后续需要手机号等字段,所以推荐 从一开始就替换用户模型。配置一旦定下来,就不要轻易改了,否则数据库迁移会有大麻烦。

django_ecommerce/settings.py 末尾添加:

# 自定义用户模型
AUTH_USER_MODEL = 'users.User'

配置好之后,Django 的内置认证系统就会以我们 users app 下的 User 模型为基准。


四、动手编写模型代码

下面我们依次编辑各 app 下的 models.py。我会把每个字段的作用解释清楚。

4.1 用户模块(apps/users/models.py)

from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    """自定义用户模型,扩展手机号字段"""
    phone = models.CharField(
        max_length=11,
        unique=True,
        null=True,
        blank=True,
        verbose_name='手机号'
    )
    email_active = models.BooleanField(
        default=False,
        verbose_name='邮箱激活状态'
    )

    class Meta:
        db_table = 'tb_users'
        verbose_name = '用户'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username


class Address(models.Model):
    """收货地址"""
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='addresses',
        verbose_name='所属用户'
    )
    receiver = models.CharField(max_length=20, verbose_name='收件人')
    phone = models.CharField(max_length=11, verbose_name='联系电话')
    province = models.CharField(max_length=20, verbose_name='省份')
    city = models.CharField(max_length=20, verbose_name='城市')
    district = models.CharField(max_length=20, verbose_name='区/县')
    detail = models.CharField(max_length=255, verbose_name='详细地址')
    is_default = models.BooleanField(
        default=False,
        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_address'
        verbose_name = '收货地址'
        verbose_name_plural = verbose_name
        ordering = ['-is_default', '-create_time']

    def __str__(self):
        return f"{self.receiver} - {self.phone}"

4.2 商品模块(apps/products/models.py)

from django.db import models


class Category(models.Model):
    """商品分类(支持无限级树形结构)"""
    name = models.CharField(max_length=50, verbose_name='分类名称')
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='children',
        verbose_name='父级分类'
    )
    level = models.PositiveSmallIntegerField(
        default=1,
        verbose_name='分类层级'
    )
    sort = models.PositiveIntegerField(
        default=0,
        verbose_name='排序值'
    )
    is_active = models.BooleanField(
        default=True,
        verbose_name='是否启用'
    )
    create_time = models.DateTimeField(
        auto_now_add=True,
        verbose_name='创建时间'
    )

    class Meta:
        db_table = 'tb_category'
        verbose_name = '商品分类'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class SPU(models.Model):
    """标准化产品单元"""
    name = models.CharField(max_length=100, verbose_name='产品名称')
    brand = models.CharField(max_length=50, null=True, blank=True, verbose_name='品牌')
    desc = models.TextField(null=True, blank=True, verbose_name='产品描述')
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
        related_name='spus',
        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_spu'
        verbose_name = 'SPU'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class SKU(models.Model):
    """库存量单位——真正可以买卖的商品"""
    spu = models.ForeignKey(
        SPU,
        on_delete=models.CASCADE,
        related_name='skus',
        verbose_name='所属 SPU'
    )
    name = models.CharField(max_length=200, verbose_name='SKU 名称')
    # 使用 JSON 字段存储规格信息,如 {"颜色":"午夜色","内存":"256G"}
    specs = models.JSONField(default=dict, verbose_name='规格信息')
    price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        verbose_name='销售价'
    )
    cost_price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        verbose_name='成本价'
    )
    stock = models.PositiveIntegerField(default=0, verbose_name='库存数量')
    sales = models.PositiveIntegerField(default=0, verbose_name='销量')
    is_active = 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_sku'
        verbose_name = 'SKU'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class ProductImage(models.Model):
    """商品图片"""
    sku = models.ForeignKey(
        SKU,
        on_delete=models.CASCADE,
        related_name='images',
        verbose_name='所属 SKU'
    )
    image = models.ImageField(
        upload_to='products/%Y/%m/',
        verbose_name='图片'
    )
    is_main = models.BooleanField(default=False, verbose_name='是否主图')
    sort = models.PositiveIntegerField(default=0, verbose_name='排序')

    class Meta:
        db_table = 'tb_product_image'
        verbose_name = '商品图片'
        verbose_name_plural = verbose_name

    def __str__(self):
        return f"{self.sku.name} 的图片"

4.3 购物车模块(apps/cart/models.py)

from django.db import models
from django.conf import settings


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
        # 一个用户对同一个 SKU 只能有一条记录
        unique_together = ('user', 'sku')

    def __str__(self):
        return f"{self.user.username} - {self.sku.name}"

4.4 订单模块(apps/orders/models.py)

from django.db import models
from django.conf import settings


class Order(models.Model):
    """订单主表"""
    # 订单状态常量
    STATUS_CHOICES = (
        (0, '待支付'),
        (1, '待发货'),
        (2, '待收货'),
        (3, '已完成'),
        (4, '已取消'),
    )

    order_no = models.CharField(
        max_length=64,
        unique=True,
        verbose_name='订单编号'
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name='orders',
        verbose_name='下单用户'
    )
    # 地址快照(存储完整地址信息,不会被后续修改影响)
    address_snapshot = models.JSONField(
        verbose_name='收货地址快照'
    )
    total_amount = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        verbose_name='订单总金额'
    )
    freight = models.DecimalField(
        max_digits=8,
        decimal_places=2,
        default=0,
        verbose_name='运费'
    )
    pay_method = models.CharField(
        max_length=20,
        default='alipay',
        verbose_name='支付方式'
    )
    status = models.SmallIntegerField(
        choices=STATUS_CHOICES,
        default=0,
        verbose_name='订单状态'
    )
    remark = models.TextField(
        null=True,
        blank=True,
        verbose_name='用户备注'
    )
    create_time = models.DateTimeField(
        auto_now_add=True,
        verbose_name='创建时间'
    )
    update_time = models.DateTimeField(
        auto_now=True,
        verbose_name='更新时间'
    )
    pay_time = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='支付时间'
    )

    class Meta:
        db_table = 'tb_order'
        verbose_name = '订单'
        verbose_name_plural = verbose_name
        ordering = ['-create_time']

    def __str__(self):
        return self.order_no


class OrderItem(models.Model):
    """订单商品条目"""
    order = models.ForeignKey(
        Order,
        on_delete=models.CASCADE,
        related_name='items',
        verbose_name='所属订单'
    )
    sku = models.ForeignKey(
        'products.SKU',
        on_delete=models.PROTECT,
        verbose_name='商品 SKU'
    )
    # 快照信息,避免商品修改后历史订单数据显示异常
    sku_name = models.CharField(max_length=200, verbose_name='商品名称')
    sku_specs = models.JSONField(verbose_name='规格快照')
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='成交单价')
    quantity = models.PositiveIntegerField(verbose_name='购买数量')

    class Meta:
        db_table = 'tb_order_item'
        verbose_name = '订单商品'
        verbose_name_plural = verbose_name

    def __str__(self):
        return f"{self.sku_name} x {self.quantity}"

4.5 支付模块(apps/payment/models.py)

from django.db import models
from django.conf import settings


class Payment(models.Model):
    """支付记录"""
    PAY_STATUS = (
        (0, '未支付'),
        (1, '支付成功'),
        (2, '支付失败'),
        (3, '已退款'),
    )

    order = models.OneToOneField(
        'orders.Order',
        on_delete=models.PROTECT,
        related_name='payment',
        verbose_name='关联订单'
    )
    trade_no = models.CharField(
        max_length=64,
        null=True,
        blank=True,
        verbose_name='支付宝流水号'
    )
    amount = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        verbose_name='支付金额'
    )
    status = models.SmallIntegerField(
        choices=PAY_STATUS,
        default=0,
        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_payment'
        verbose_name = '支付记录'
        verbose_name_plural = verbose_name

    def __str__(self):
        return f"支付记录 {self.trade_no or '待支付'}"

五、让模型生效——生成迁移与建表

模型代码只是 Python 类,要想变成数据库中的表,还得靠 Django 迁移系统。我们先确保所有代码保存完毕,然后在项目根目录执行以下命令。

5.1 生成迁移文件

python manage.py makemigrations

控制台输出示例:

Migrations for 'cart':
  apps/cart/migrations/0001_initial.py
    - Create model CartItem
Migrations for 'orders':
  apps/orders/migrations/0001_initial.py
    - Create model Order
    - Create model OrderItem
Migrations for 'payment':
  apps/payment/migrations/0001_initial.py
    - Create model Payment
Migrations for 'products':
  apps/products/migrations/0001_initial.py
    - Create model Category
    - Create model SPU
    - Create model SKU
    - Create model ProductImage
Migrations for 'users':
  apps/users/migrations/0001_initial.py
    - Create model User
    - Create model Address

Django 会为每个 app 生成一个 0001_initial.py 迁移文件,记录了我们写的所有模型。如果出现报错,比如忘记安装 Pillow 导致 ImageField 无法使用,可以先 pip install Pillow 再执行命令。

5.2 应用到数据库

控制台输出示例:

Operations to perform:
  Apply all migrations: admin, auth, cart, contenttypes, orders, payment, products, sessions, users
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  ...
  Applying users.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying cart.0001_initial... OK
  Applying orders.0001_initial... OK
  Applying payment.0001_initial... OK
  Applying products.0001_initial... OK
  Applying sessions.0001_initial... OK

看到一连串的 OK,代表所有表都已经在数据库中创建成功了。

5.3 检查生成的表

Django 默认使用 SQLite,数据存储在项目根目录下的 db.sqlite3 文件中。我们可以用 dbshell 命令直接连进去看一眼:

进入 sqlite shell 后,输入 .tables 查看所有表:

输出(关键部分):

auth_group                  tb_category
auth_group_permissions      tb_sku
auth_permission             tb_spu
auth_user                   tb_product_image
auth_user_groups            tb_cart_item
auth_user_user_permissions  tb_order
django_admin_log            tb_order_item
django_content_type         tb_payment
django_migrations           tb_address
django_session              tb_users

可以看到,除了 Django 自带的 auth_*django_* 表外,我们自定义的 tb_userstb_addresstb_categorytb_sputb_skutb_product_imagetb_cart_itemtb_ordertb_order_itemtb_payment 都已经整整齐齐地出现在数据库里了。

提示:SQLite 查看表结构可以用 .schema tb_users 等命令,这里就不一一演示了。


六、总结与下集预告

今天我们花了大力气把整个电商平台的数据库“地基”打好了,完成了:

  • 从用户角色到功能模块的完整需求分析;

  • 梳理出 10 个核心实体,并明确了它们之间的关系;

  • 配置了自定义用户模型 AUTH_USER_MODEL

  • 在五个 app 中编写了完整的模型代码;

  • 成功执行了 makemigrationsmigrate,在数据库中创建了所有业务表。

现在你的项目已经有模有样了:打开 db.sqlite3,就能看到一张张按照我们心意设计的表。

但模型里还有很多细节值得深挖:on_delete 的六种行为有什么区别?related_name 到底怎么用?Django 迁移系统背后的原理是什么? 第 3 篇,我将带大家深入 Django 模型的进阶用法,同时完善字段的参数、索引、元选项等,并且演示数据迁移的高级技巧——包括怎么修改一个已有数据的表结构而不丢数据。记得准时来看!

有任何疑问欢迎在评论区留言,IT策士 会第一时间回复。如果觉得这系列靠谱,点个赞、收藏一下,我们下一篇见!

还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !