> 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ā)的主線:
用戶注冊并登錄;
瀏覽商品列表 / 搜索商品 / 查看詳情;
將中意的 SKU 加入購物車;
在購物車頁面確認商品、數(shù)量,點擊“去結(jié)算”;
進入確認訂單頁,選擇收貨地址,提交訂單;
跳轉(zhuǎn)支付寶沙箱付款;
支付成功后返回,查看我的訂單。
這套流程會把我們所有的 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 AddressDjango 會為每個 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_address、tb_category、tb_spu、tb_sku、tb_product_image、tb_cart_item、tb_order、tb_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í)行了
makemigrations與migrate,在數(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 思維 !