表结构设计


表结构设计是一个项目的基石,组织好各种数据之间的逻辑关系,往往能够使开发事半功倍。

一、权限控制表结构设计

UserInfo

任何项目都要用人使用才有价值,因此设计好用户信息表是第一步。

Django 提供了一个 AbstractUser 类,可以在这个类的基础之上定制我们需要的 model。

我们来看一下这个类的部分源码:

username :用户名

username = models.CharField(
_('username'),
max_length=150,
unique=True,
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
validators=[username_validator],
error_messages={
'unique': _("A user with that username already exists."),
},
)

first_name:名、last_name :姓

first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=150, blank=True)

email :邮箱

email = models.EmailField(_('email address'), blank=True)

is_staff :是否为员工

is_staff = models.BooleanField(
_('staff status'),
default=False,
help_text=_('Designates whether the user can log into this admin site.'),
)

is_active :是否处于活动状态

is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)

date_joined :加入日期

date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

除了 AbstractUser 帮我们定义好的这些属性之外,我们还要自定义一些属性:

gender :成员性别

gender = models.IntegerField(verbose_name='性别', choices=((1, '男'), (2, '女')), default=1)

avatar:成员头像

avatar = models.ImageField(upload_to='avatars/', default='avatars/default.png')

telephone :成员手机号

telephone = models.CharField(max_length=11, null=True, unique=True)

roles:成员所拥有的角色

一个成员可以拥有多个角色,一个角色也可使赋予多个成员,因此 roles 字段应该是多对多的结构。

roles = models.ManyToManyField(verbose_name='拥有的所有角色', to="Role", blank=True)

depart :成员所属部门

一个department只能属于一个部门,根据角色的不同可以拥有该部门负责事务内的权限,而一个部门可以拥有多名成员,因此成员与部门之间是一对多的关系。

department = models.ForeignKey(verbose_name='部门', to="Department", on_delete=models.PROTECT)

organize :成员所属组织

一个成员只能属于一个组织,而一个组织可以拥有多名成员,因此成员与组织之间是一对多的关系。

organize = models.ForeignKey(verbose_name="组织", to="Organize", null=True, blank=True, on_delete=models.PROTECT)

最后,我们的目的想在做这个项目的同时开发出通用的、达到对象级别的权限控制组件,因此我们只借用 AbstractUser 字段而不继承它,确定 UserInfo model 为:

# RBAC/models.py
class UserInfo(models.Model):
"""成员信息"""
username = models.CharField(verbose_name="用户名", max_length=150, unique=True)
first_name = models.CharField(verbose_name="名", max_length=30, blank=True)
last_name = models.CharField(verbose_name="姓", max_length=150, blank=True)
email = models.EmailField(verbose_name="邮箱", blank=True)
score = models.IntegerField(verbose_name="积分", default=10)
grade = models.IntegerField(verbose_name="等级", choices=((1, "M1"), (2, "M2"), (3, "M3"), (4, "M4"), (5, "M5")),
default=1)
gender = models.IntegerField(verbose_name='性别', choices=((1, '男'), (2, '女')), default=1)
avatar = models.ImageField(verbose_name="头像", upload_to='avatars/', default='avatars/default.png')
telephone = models.CharField(verbose_name='手机号', max_length=11, null=True, unique=True)
date_joined = models.DateTimeField(verbose_name="加入日期", default=timezone.now)

roles = models.ManyToManyField(verbose_name='拥有的所有角色', to="Role", blank=True)
team = models.ForeignKey(verbose_name="组", to="Team", null=True, blank=True, on_delete=models.PROTECT)
department = models.ForeignKey(verbose_name='部门', to="Department", null=True, blank=True, on_delete=models.PROTECT)

def __str__(self):
return self.username

ScoreRecord

工作室共成员分为5个等级,由个人等级积分及相关条件确定,详情见下表:

等级

积分要求

其他要求

备注

M1 (white)

10

新成员初始积分10分

M2 (blue)

100

通过基础考试

线上答题,随时可尝试

M3 (yellow)

1000

至少参与3个项目

工作室项目或自己的项目

M4 (orange)

10000

能够带队完成项目

限定工作室项目

M5 (red)

100000

有开源框架贡献

在GitHub、CSDN、博客园等社区有一定知名度

# RBAC/models.py
class ScoreRecord(models.Model):
"""积分记录"""
score = models.IntegerField(verbose_name="处理分值")
reason = models.TextField(verbose_name="理由")
member = models.ForeignKey(verbose_name="成员", to="UserInfo", on_delete=models.PROTECT)
referee = models.ForeignKey(verbose_name="执行人", to="UserInfo", on_delete=models.PROTECT)

Attendance

class Attendance(models.Model):
"""出勤记录"""
subject = models.CharField(verbose_name="主题", max_length=32)
member = models.ForeignKey(verbose_name="成员", to="UserInfo", on_delete=models.PROTECT)
record = models.CharField("记录", choices=[("check", "全勤"), ("vacate", "请假"), ("late", "迟到"), ("lack", "缺勤")])
score = models.IntegerField(verbose_name="处理分值")
referee = models.ForeignKey(verbose_name="执行人", to="UserInfo", on_delete=models.PROTECT)

def __str__(self):
return "%s-%s" % (self.subject, self.member.username)

Matrix工作室划分为6个部门、4个小组,每位成员可同时拥有所属部门和所属分组。


Team

工作室共分四个组:1. 算法组、2. 前端组、3. 后端组、4. AI组

# RBAC/models.py
class Team(models.Model):
"""分组信息"""
teamName = models.CharField(verbose_name="Team名称", max_length=32, unique=True)
introduce = models.TextField(verbose_name="Team介绍")
# 一个组内可以拥有多名角色,但一个角色只能属于一个组
hasRoles = models.ForeignKey(verbose_name="组内拥有的角色", to="Role", null=True, blank=True, on_delete=models.PROTECT)

def __str__(self):
return self.teamName

Department

工作室下设六个部门:

1. 项目商谈部

由各组组长组成,负责与甲方商谈项目的需求功能与出价,整理出具体的需求分析报告或导图。

2. 项目开发部

负责每个项目的进度监督、成员安排、整体架构设计和技术解决方案,合理调配各组成员。

3. UI设计部

与项目开发人员沟通,负责前端、移动端页面设计,负责工作室宣传海报、视频的制作。

4. 学院联系

负责与学院相关部门建立联系,维护工作室为学院制作的有关项目,并负责各种比赛的报名与培训安排。

5. 成员管理部

统一管理各组成员,每月团建,同时负责新成员的培训与学习监督,各组成员之间的流动。

6. 技术委员会

由各组组长和工作室M4、M5级别成员组成,为工作室提供技术支持和技术评审。

# RBAC/models.py
class Department(models.Model):
"""部门信息"""
departmentName = models.CharField(verbose_name="部门名称", max_length=32, unique=True)
duty = models.TextField(verbose_name="部门职责")
# 一个部门可以拥有多名角色,但一个角色只能属于一个部门
hasRoles = models.ForeignKey(verbose_name="组内拥有的角色", to="Role", null=True, blank=True, on_delete=models.PROTECT)

def __str__(self):
return self.departmentName

Role

通过角色将成员与权限之间关联起来,不同的成员拥有不同的角色,不同的角色拥有不同的权限。

# RBAC/models.py
class Role(models.Model):
"""角色"""
roleName = models.CharField(verbose_name="角色名称", max_length=32)

permissions = models.ManyToManyField(verbose_name="角色所拥有权限", to='Permission', null=True, blank=True)

def __str__(self):
return self.roleName

Permission

权限其实就是成员是否具有访问某个 URL 的资格,因此权限的主要字段其实就是 URL。

# RBAC/models.py
class Permission(models.Model):
"""权限"""
url = models.CharField(verbose_name="权限URL正则表达式", max_length=256)
permissionName = models.CharField(verbose_name="权限名称", max_length=32)
alias = models.CharField(verbose_name="权限URL别名", max_length=32, unique=True)
icon = models.CharField(verbose_name="权限图标", max_length=32)

menu = models.ForeignKey(verbose_name="所属菜单", to="Menu", null=True, blank=True, on_delete=models.PROTECT,
help_text="如果为 null 表示该权限不是菜单,否则为二级菜单")
parentPermission = models.ForeignKey(verbose_name="父权限", to="Permission", null=True, blank=True,
related_name="parentPermission", on_delete=models.PROTECT,
help_text="非菜单权限需要一个二级菜单的父权限做默认展开和选中")

def __str__(self):
return self.permissionName

Menu

菜单用于侧边栏展示。

# RBAC/models.py
class Menu(models.Model):
"""菜单"""
menuName = models.CharField(verbose_name="菜单名称", max_length=32)
icon = models.CharField(verbose_name="菜单图标", max_length=32)

def __str__(self):
return self.menuName

二、业务表结构设计

User

对于非工作室人员注册的账号,其实就是游客,他们也需要一个账号,另外,工作室内部成员有时也是游客,此时,需要为游客创建一张用户表,这时候,我们就可以直接用 AbstractUser 类做继承了:

class User(AbstractUser):
"""用户"""
avatar = models.ImageField(upload_to='avatars/', default='avatars/default.png')
telephone = models.CharField(max_length=11, null=True, blank=True, unique=True)

def __str__(self):
return self.username

Course

工作室 M4、M5等级的成员可以开设课程,为方便管理,需要创建一张 Course 表。

# index/models.py
class Course(models.Model):
"""课程"""
courseName = models.CharField(verbose_name="课程名称", max_length=32)
sketch = models.TextField(verbose_name="课程简述")
price = models.PositiveIntegerField(verbose_name="学费", help_text="游客学习收费,工作室成员学习免费")
cover = models.ImageField(verbose_name="课程封面", upload_to='courseCover/', default='courseCover/default.png')
grade = models.IntegerField(verbose_name="课程等级", choices=((1, "M1"), (2, "M2"), (3, "M3"), (4, "M4"), (5, "M5")))
category = models.IntegerField(verbose_name="课程分类", choices=((1, "算法"), (2, "前端"), (3, "后端"), (4, "AI"), (5, "其它")))

teacher = models.ForeignKey(verbose_name="开课老师", to="UserInfo", on_delete=models.PROTECT,
help_text="开课老师限制为M4、M5等级成员")
assistant = models.ManyToManyField(verbose_name="助教", to="UserInfo",
help_text="助教限定为M3等级成员")

def __str__(self):
return self.courseName

Classes

每一门课程对应一个班级,用于存储一些课程资料、学生交流和老师答疑。

# index/models.py
class Classes(models.Model):
"""班级"""
startDate = models.DateField(verbose_name="开课日期")
QQ = models.IntegerField(verbose_name="班级QQ群")
graduateDate = models.DateField(verbose_name="结业日期", null=True, blank=True)
explain = models.TextField(verbose_name="说明", null=True, blank=True)

course = models.ForeignKey(verbose_name="课程", to="Course", on_delete=models.PROTECT)
classTeacher = models.ForeignKey(verbose_name="班主任", to="UserInfo", on_delete=models.PROTECT,
help_text="班主任为成员管理部成员,负责督促老师课程制作进度和学生学习进度")

def __str__(self):
return "%s-%s" % (self.course.courseName, self.QQ)

Student

class Student(models.Model):
"""学生表"""
student = models.OneToOneField(verbose_name="学生信息", to="User", on_delete=models.PROTECT)
QQ = models.CharField(verbose_name="学生QQ", max_length=32)
telephone = models.IntegerField(verbose_name="学生手机号", max_length=32)
classList = models.ManyToManyField(verbose_name="已报班级", to="Classes", null=True, blank=True)
state = models.IntegerField(verbose_name="学生状态", choices=[(1, "审核"), (2, "在读"), (3, "毕业")], default=1)
remark = models.TextField(verbose_name="备注")

def __str__(self):
return "%s-%s" % (self.student.username, self.classList.course.name)

Project

工作室承接项目,需要一个项目表存储项目记录。

class Project(models.Model):
"""项目"""
name = models.CharField(verbose_name="项目名称", max_length=32)
contactName = models.CharField(verbose_name="联系人姓名", max_length=32)
contactInformation = models.CharField(verbose_name="联系人联系方式", max_length=64, help_text="QQ/WeChat/Phone")
price = models.IntegerField(verbose_name="项目报价")
introduce = models.TextField(verbose_name="项目介绍")

superintendent = models.ForeignKey(verbose_name="项目负责人", to="UserInfo",
null=True, blank=True, on_delete=models.PROTECT)
startDate = models.DateField(verbose_name="接取日期", null=True, blank=True)
completeDate = models.DateField(verbose_name="完结日期", null=True, blank=True)

def __str__(self):
return self.name

ProjectRecord

为了保证项目能够保质保量的按时完成,每个项目需要一名项目开发部的成员进行监督和跟进。

class ProjectRecord(models.Model):
"""项目跟进记录"""
content = models.TextField(verbose_name="跟进内容")
date = models.DateField(verbose_name="跟进日期", default=timezone.now)

project = models.ForeignKey(verbose_name="跟进项目", to="Project", on_delete=models.PROTECT)
superintendent = models.ForeignKey(verbose_name="跟进人", to="UserInfo", on_delete=models.PROTECT)

def __str__(self):
return "%s-%s" % (self.project.name, self.date)

PaymentRecord

class PaymentRecord(models.Model):
"""账单记录"""
user = models.OneToOneField(verbose_name="付款人", to="User", on_delete=models.PROTECT)
type = models.IntegerField(verbose_name="账单类型", choices=[(1, "学费"), (2, "项目款"), (3, "其它")])
price = models.IntegerField(verbose_name="金额")
date = models.DateTimeField(verbose_name="账单日期", default=timezone.now)
state = models.IntegerField(verbose_name="状态", choices=[(1, "审核中"), (2, "确认"), (3, "驳回")])
confirmDate = models.DateTimeField(verbose_name="确认日期", null=True, blank=True)
confirmUser = models.ForeignKey(verbose_name="审批人", to="UserInfo", null=True, blank=True, on_delete=models.PROTECT)
remark = models.TextField(verbose_name="备注", null=True, blank=True)

def __str__(self):
return "%s-%s-%s" % (self.user.username, self.type, self.price)