IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。
电商项目需求分析与数据库设计
各位小伙伴好,我是IT策士。上一节我们搭好了项目骨架,创建了 users、products、cart、orders、payment 五个 app,并且让开发服务器跑了起来。今天我们要做一个项目里 最不能马虎 的环节------需求分析与数据库设计。数据库是整个系统的地基,地基歪了,后面所有的功能都会"塌方"。
接下来我们会先把电商平台的核心业务梳理清楚,然后画出实体关系图,最后动手把 Django 的模型代码写出来并真正建表。
一、电商平台需求分析
1.1 用户角色
一个典型的电商平台至少包含两类角色:
-
普通用户(买家):浏览商品、加入购物车、下单、支付、查看订单、管理地址。
-
管理员:通过 Django Admin 管理商品、分类、订单、用户。
本系列主要实现的是 面向买家 的电商系统,管理员的功能会大量依赖 Django Admin 来完成,不会单独开发一套管理后台。
1.2 核心功能模块
拆解成六大模块,正好对应我们创建的五个 app + 一个抽象层(支付虽独立 app,但属于订单模块的延伸):
1.3 业务流程主线
一个完整的购物流程大致如下,这也是我们后面开发的主线:
-
用户注册并登录;
-
浏览商品列表 / 搜索商品 / 查看详情;
-
将中意的 SKU 加入购物车;
-
在购物车页面确认商品、数量,点击"去结算";
-
进入确认订单页,选择收货地址,提交订单;
-
跳转支付宝沙箱付款;
-
支付成功后返回,查看我的订单。
这套流程会把我们所有的 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_users、tb_address、tb_category、tb_spu、tb_sku、tb_product_image、tb_cart_item、tb_order、tb_order_item、tb_payment 都已经整整齐齐地出现在数据库里了。
提示:SQLite 查看表结构可以用
.schema tb_users等命令,这里就不一一演示了。
六、总结与下集预告
今天我们花了大力气把整个电商平台的数据库"地基"打好了,完成了:
-
从用户角色到功能模块的完整需求分析;
-
梳理出 10 个核心实体,并明确了它们之间的关系;
-
配置了自定义用户模型
AUTH_USER_MODEL; -
在五个 app 中编写了完整的模型代码;
-
成功执行了
makemigrations与migrate,在数据库中创建了所有业务表。
现在你的项目已经有模有样了:打开 db.sqlite3,就能看到一张张按照我们心意设计的表。
但模型里还有很多细节值得深挖:on_delete 的六种行为有什么区别?related_name 到底怎么用?Django 迁移系统背后的原理是什么? 第 3 篇,我将带大家深入 Django 模型的进阶用法,同时完善字段的参数、索引、元选项等,并且演示数据迁移的高级技巧------包括怎么修改一个已有数据的表结构而不丢数据。记得准时来看!
有任何疑问欢迎在评论区留言,IT策士 会第一时间回复。如果觉得这系列靠谱,点个赞、收藏一下,我们下一篇见!
还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !