第四章:数据库设计与建模

好,咱们进入第四章。这一章可以说是整个ERP系统的地基。地基没打好,后面盖多少层楼都得塌。我见过太多项目,前期数据库设计随便搞搞,结果后期改得死去活来。嗯,咱们不犯这个错。

4.1 从业务到数据:ER图怎么画?

先问个问题:你拿到一个ERP需求,第一步做什么?

我个人习惯,不是写代码,而是拿张白纸,画框框和连线。这就是ER图——实体关系图。

说白了,ER图就是把现实世界的业务对象,抽象成数据模型。比如:

  • 实体:用户、部门、产品、订单……这些是名词
  • 属性:用户名、密码、邮箱……这些是实体的特征
  • 关系:一个部门有多个用户,一个用户属于一个部门

我在项目中遇到过最典型的坑:把「关系」当成了「属性」。比如直接在用户表里加一个「部门名称」字段。这看起来省事,但部门改名时你就得改所有用户记录。嗯,这就是没画ER图的后果。

核心原则:先画关系,再定属性。关系对了,表结构就稳了。

4.2 核心数据表结构设计

咱们这个ERP系统,核心表就那么几张。我按模块一个一个说。

4.2.1 用户与权限体系

用户、部门、角色、权限,这四个是基础中的基础。我建议用RBAC模型(基于角色的访问控制)。

用户表(users)

字段名类型说明
idINT (PK)主键
usernameVARCHAR(50)用户名,唯一
password_hashVARCHAR(128)密码哈希值,别存明文
emailVARCHAR(100)邮箱
department_idINT (FK)所属部门
is_activeBOOLEAN是否启用
created_atDATETIME创建时间

部门表(departments)

字段名类型说明
idINT (PK)主键
nameVARCHAR(50)部门名称
parent_idINT (FK, nullable)上级部门,支持树形结构

角色表(roles)

字段名类型说明
idINT (PK)主键
nameVARCHAR(50)角色名,如管理员、采购员
descriptionTEXT角色描述

权限表(permissions)

字段名类型说明
idINT (PK)主键
codenameVARCHAR(100)权限代码,如 'can_add_product'
nameVARCHAR(100)权限名称

你想想看,用户和角色是多对多,角色和权限也是多对多。所以需要两张中间表:user_rolesrole_permissions

我的经验:权限代码命名要规范。我习惯用「动词_模块_对象」的格式,比如 view_orderedit_product。这样后期维护时一眼就能看懂。

4.2.2 产品与库存

产品表是ERP的核心。库存表则记录每个产品的实时数量。

产品表(products)

字段名类型说明
idINT (PK)主键
skuVARCHAR(50)库存单位编码,唯一
nameVARCHAR(200)产品名称
category_idINT (FK)分类
unit_priceDECIMAL(10,2)单价
cost_priceDECIMAL(10,2)成本价
descriptionTEXT描述

库存表(inventory)

字段名类型说明
idINT (PK)主键
product_idINT (FK)关联产品
warehouse_idINT (FK)仓库
quantityINT当前库存数量
last_updatedDATETIME最后更新时间

注意:千万不要直接用产品表里的字段存库存数量。库存是动态的,产品信息是静态的。混在一起,并发操作时数据会乱。我曾经见过一个项目,库存扣减时直接 UPDATE products SET stock = stock - 1,结果并发下单导致库存变负数。嗯,血的教训。

4.2.3 订单与订单明细

订单表记录一次交易,订单明细表记录这次交易买了哪些产品。

订单表(orders)

字段名类型说明
idINT (PK)主键
order_numberVARCHAR(50)订单号,唯一
user_idINT (FK)下单用户
statusVARCHAR(20)状态:待支付、已支付、已发货等
total_amountDECIMAL(12,2)总金额
created_atDATETIME下单时间

订单明细表(order_items)

字段名类型说明
idINT (PK)主键
order_idINT (FK)所属订单
product_idINT (FK)产品
quantityINT数量
unit_priceDECIMAL(10,2)成交单价(可能和产品表不同)
subtotalDECIMAL(12,2)小计金额

为什么会把单价存两份?因为产品价格会变。订单生成时的价格必须固定下来,不能跟着产品表变。这是设计上的一个细节。

4.3 Django ORM 模型定义

好,表结构设计完了,接下来用代码实现。我用Django ORM举例,Flask-SQLAlchemy思路一样,语法略有不同。

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

class Department(models.Model):
    name = models.CharField(max_length=50)
    parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.name

class User(AbstractUser):
    department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True)
    phone = models.CharField(max_length=20, blank=True)

class Role(models.Model):
    name = models.CharField(max_length=50)
    description = models.TextField(blank=True)
    permissions = models.ManyToManyField('Permission')

class Permission(models.Model):
    codename = models.CharField(max_length=100, unique=True)
    name = models.CharField(max_length=100)

class Product(models.Model):
    sku = models.CharField(max_length=50, unique=True)
    name = models.CharField(max_length=200)
    category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
    unit_price = models.DecimalField(max_digits=10, decimal_places=2)
    cost_price = models.DecimalField(max_digits=10, decimal_places=2)
    description = models.TextField(blank=True)

class Inventory(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    warehouse = models.ForeignKey('Warehouse', on_delete=models.CASCADE)
    quantity = models.IntegerField(default=0)
    last_updated = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ('product', 'warehouse')

class Order(models.Model):
    order_number = models.CharField(max_length=50, unique=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    status = models.CharField(max_length=20, default='pending')
    total_amount = models.DecimalField(max_digits=12, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.IntegerField()
    unit_price = models.DecimalField(max_digits=10, decimal_places=2)
    subtotal = models.DecimalField(max_digits=12, decimal_places=2)

说个小技巧:Django的 auto_now_add=True 会在创建时自动填入当前时间,auto_now=True 会在每次保存时更新。别搞混了。我刚开始用Django时,把两个参数写反了,结果创建时间每次更新都被改掉。排查了半天才发现。

4.4 模型关系总结

咱们把关系理一理:

  • 用户 ↔ 部门:多对一。一个部门有多个用户,一个用户属于一个部门。
  • 用户 ↔ 角色:多对多。一个用户可以有多个角色,一个角色可以分配给多个用户。
  • 角色 ↔ 权限:多对多。一个角色拥有多个权限,一个权限可以属于多个角色。
  • 产品 ↔ 库存:一对多。一个产品在多个仓库有库存,一个库存记录对应一个产品。
  • 订单 ↔ 订单明细:一对多。一个订单包含多个商品项。

这些关系,在ER图上就是连线。画清楚了,代码写起来就顺了。

最后提醒一句:数据库设计没有完美的,只有适合的。别追求过度范式化,适当冗余可以提高查询效率。但也不要为了省事乱加字段。平衡点在哪里?嗯,多踩几次坑就知道了。

下一章,咱们开始搭建开发环境,把Django项目跑起来。到时候这些模型就能真正用上了。