Django 從 0 到 1 打造完整電商平臺:電商項目需求分析與數(shù)據(jù)庫設計

> IT策士 10余年一線大廠經(jīng)驗,專注 IT 思維、架構(gòu)、職場進階。我會在公眾號、今日頭條持續(xù)發(fā)布最新文章,助你少走彎路。

電商項目需求分析與數(shù)據(jù)庫設計

各位小伙伴好,我是IT策士。上一節(jié)我們搭好了項目骨架,創(chuàng)建了 users、products、cart、orders、payment 五個 app,并且讓開發(fā)服務器跑了起來。今天我們要做一個項目里 最不能馬虎 的環(huán)節(jié)——需求分析與數(shù)據(jù)庫設計。數(shù)據(jù)庫是整個系統(tǒng)的地基,地基歪了,后面所有的功能都會“塌方”。

接下來我們會先把電商平臺的核心業(yè)務梳理清楚,然后畫出實體關(guān)系圖,最后動手把 Django 的模型代碼寫出來并真正建表。


一、電商平臺需求分析

1.1 用戶角色

一個典型的電商平臺至少包含兩類角色:

  • 普通用戶(買家):瀏覽商品、加入購物車、下單、支付、查看訂單、管理地址。

  • 管理員:通過 Django Admin 管理商品、分類、訂單、用戶。

本系列主要實現(xiàn)的是 面向買家 的電商系統(tǒng),管理員的功能會大量依賴 Django Admin 來完成,不會單獨開發(fā)一套管理后臺。

1.2 核心功能模塊

拆解成六大模塊,正好對應我們創(chuàng)建的五個 app + 一個抽象層(支付雖獨立 app,但屬于訂單模塊的延伸):

1.3 業(yè)務流程主線

一個完整的購物流程大致如下,這也是我們后面開發(fā)的主線:

  1. 用戶注冊并登錄;

  2. 瀏覽商品列表 / 搜索商品 / 查看詳情;

  3. 將中意的 SKU 加入購物車;

  4. 在購物車頁面確認商品、數(shù)量,點擊“去結(jié)算”;

  5. 進入確認訂單頁,選擇收貨地址,提交訂單;

  6. 跳轉(zhuǎn)支付寶沙箱付款;

  7. 支付成功后返回,查看我的訂單。

這套流程會把我們所有的 app 串聯(lián)起來,因此數(shù)據(jù)庫設計必須能承載這些流轉(zhuǎn)的數(shù)據(jù)。


二、數(shù)據(jù)庫設計——從 E-R 圖到數(shù)據(jù)表

2.1 核心實體梳理

根據(jù)需求,我們可以抽象出以下幾個核心實體:

  • 用戶(User)

  • 收貨地址(Address)

  • 商品分類(Category)

  • 商品 SPU(Standard Product Unit)

  • 商品 SKU(Stock Keeping Unit)——實際售賣的單位

  • 商品圖片(ProductImage)

  • 購物車條目(CartItem)

  • 訂單(Order)

  • 訂單商品條目(OrderItem)

  • 支付記錄(Payment)

名詞解釋:SPU 代表“標準化產(chǎn)品單元”,比如 iPhone 15;SKU 代表“庫存量單位”,是具體到規(guī)格的商品,比如“iPhone 15 128G 午夜色”。一款 SPU 下可以有多個 SKU。

2.2 實體間關(guān)系(E-R 圖)

因為 Markdown 不能直接畫圖,我用最直白的“連線描述法”來表示:

[用戶] 1 ──── N [收貨地址]         (一個用戶有多個地址)
[商品分類] 1 ──── N [商品分類]     (自關(guān)聯(lián),實現(xià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 [支付記錄]

注意:訂單里的收貨地址我們會做“快照”處理,不直接外鍵關(guān)聯(lián)地址表,防止用戶修改地址后影響歷史訂單。


三、配置 AUTH_USER_MODEL(非常重要?。?/span>

Django 自帶的 User 模型字段有限,而我們后續(xù)需要手機號等字段,所以推薦 從一開始就替換用戶模型。配置一旦定下來,就不要輕易改了,否則數(shù)據(jù)庫遷移會有大麻煩。

django_ecommerce/settings.py 末尾添加:

# 自定義用戶模型
AUTH_USER_MODEL = 'users.User'

配置好之后,Django 的內(nèi)置認證系統(tǒng)就會以我們 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='郵箱激活狀態(tài)'
    )

    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='聯(lián)系電話')
    province = models.CharField(max_length=20, verbose_name='省份')
    city = models.CharField(max_length=20, verbose_name='城市')
    district = models.CharField(max_length=20, verbose_name='區(qū)/縣')
    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='創(chuàng)建時間'
    )
    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):
    """商品分類(支持無限級樹形結(jié)構(gòu))"""
    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='創(chuàng)建時間'
    )

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

    def __str__(self):
        return self.name


class SPU(models.Model):
    """標準化產(chǎn)品單元"""
    name = models.CharField(max_length=100, verbose_name='產(chǎn)品名稱')
    brand = models.CharField(max_length=50, null=True, blank=True, verbose_name='品牌')
    desc = models.TextField(null=True, blank=True, verbose_name='產(chǎn)品描述')
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
        related_name='spus',
        verbose_name='所屬分類'
    )
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='創(chuàng)建時間')
    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 字段存儲規(guī)格信息,如 {"顏色":"午夜色","內(nèi)存":"256G"}
    specs = models.JSONField(default=dict, verbose_name='規(guī)格信息')
    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='庫存數(shù)量')
    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='創(chuàng)建時間')
    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='購買數(shù)量'
    )
    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):
    """訂單主表"""
    # 訂單狀態(tài)常量
    STATUS_CHOICES = (
        (0, '待支付'),
        (1, '待發(fā)貨'),
        (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='下單用戶'
    )
    # 地址快照(存儲完整地址信息,不會被后續(xù)修改影響)
    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='訂單狀態(tài)'
    )
    remark = models.TextField(
        null=True,
        blank=True,
        verbose_name='用戶備注'
    )
    create_time = models.DateTimeField(
        auto_now_add=True,
        verbose_name='創(chuàng)建時間'
    )
    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'
    )
    # 快照信息,避免商品修改后歷史訂單數(shù)據(jù)顯示異常
    sku_name = models.CharField(max_length=200, verbose_name='商品名稱')
    sku_specs = models.JSONField(verbose_name='規(guī)格快照')
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='成交單價')
    quantity = models.PositiveIntegerField(verbose_name='購買數(shù)量')

    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='關(guān)聯(lián)訂單'
    )
    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='支付狀態(tài)'
    )
    create_time = models.DateTimeField(
        auto_now_add=True,
        verbose_name='創(chuàng)建時間'
    )
    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 類,要想變成數(shù)據(jù)庫中的表,還得靠 Django 遷移系統(tǒng)。我們先確保所有代碼保存完畢,然后在項目根目錄執(zhí)行以下命令。

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 遷移文件,記錄了我們寫的所有模型。如果出現(xiàn)報錯,比如忘記安裝 Pillow 導致 ImageField 無法使用,可以先 pip install Pillow 再執(zhí)行命令。

5.2 應用到數(shù)據(jù)庫

控制臺輸出示例:

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,代表所有表都已經(jīng)在數(shù)據(jù)庫中創(chuàng)建成功了。

5.3 檢查生成的表

Django 默認使用 SQLite,數(shù)據(jù)存儲在項目根目錄下的 db.sqlite3 文件中。我們可以用 dbshell 命令直接連進去看一眼:

進入 sqlite shell 后,輸入 .tables 查看所有表:

輸出(關(guān)鍵部分):

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_users、tb_addresstb_category、tb_sputb_sku、tb_product_imagetb_cart_item、tb_ordertb_order_item、tb_payment 都已經(jīng)整整齊齊地出現(xiàn)在數(shù)據(jù)庫里了。

提示:SQLite 查看表結(jié)構(gòu)可以用 .schema tb_users 等命令,這里就不一一演示了。


六、總結(jié)與下集預告

今天我們花了大力氣把整個電商平臺的數(shù)據(jù)庫“地基”打好了,完成了:

  • 從用戶角色到功能模塊的完整需求分析;

  • 梳理出 10 個核心實體,并明確了它們之間的關(guān)系;

  • 配置了自定義用戶模型 AUTH_USER_MODEL;

  • 在五個 app 中編寫了完整的模型代碼;

  • 成功執(zhí)行了 makemigrationsmigrate,在數(shù)據(jù)庫中創(chuàng)建了所有業(yè)務表。

現(xiàn)在你的項目已經(jīng)有模有樣了:打開 db.sqlite3,就能看到一張張按照我們心意設計的表。

但模型里還有很多細節(jié)值得深挖:on_delete 的六種行為有什么區(qū)別?related_name 到底怎么用?Django 遷移系統(tǒng)背后的原理是什么? 第 3 篇,我將帶大家深入 Django 模型的進階用法,同時完善字段的參數(shù)、索引、元選項等,并且演示數(shù)據(jù)遷移的高級技巧——包括怎么修改一個已有數(shù)據(jù)的表結(jié)構(gòu)而不丟數(shù)據(jù)。記得準時來看!

有任何疑問歡迎在評論區(qū)留言,IT策士 會第一時間回復。如果覺得這系列靠譜,點個贊、收藏一下,我們下一篇見!

> 還可以去公眾號、今日頭條搜索「IT策士」,一起升級 IT 思維 !

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容