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

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 不能直接画图,我用最直白的"连线描述法"来表示:

bash 复制代码
[用户] 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 末尾添加:

bash 复制代码
# 自定义用户模型
AUTH_USER_MODEL = 'users.User'

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


四、动手编写模型代码

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

4.1 用户模块(apps/users/models.py)
bash 复制代码
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)
bash 复制代码
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)
bash 复制代码
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)
bash 复制代码
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)
bash 复制代码
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 生成迁移文件
bash 复制代码
python manage.py makemigrations

控制台输出示例:

bash 复制代码
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 应用到数据库

控制台输出示例:

bash 复制代码
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 查看所有表:

输出(关键部分):

bash 复制代码
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 思维 !

相关推荐
憧憬成为web高手11 小时前
皮卡丘靶场速通--sql 2
数据库·sql·mybatis
段一凡-华北理工大学12 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
oradh14 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
z1234567898615 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD15 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
TlSfoward16 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
数智化管理手记16 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘
LabVIEW开发16 小时前
NI Package Manager安装LabVIEW时InternalServerError错误
网络·数据库·labview·labview知识·labview功能·labview程序
SelectDB16 小时前
PostgreSQL 实时同步到 Apache Doris:一站式 CDC 解决方案
数据库
欲速不达882016 小时前
Mysql并发DML锁等待的优化
数据库·mysql