Django实战【二】—项目应用分离和整体设计思路
一、项目应用分离
上篇博客我们已经分析了我们的AliCRM项目针对的用户群体,以及具体的业务范围,根据我们设计的具体需求
如需要源码,从这里下载,代码纯属学习所写,如果能够提供参考,十分高兴: https://github.com/ryxiong728/ObCRM.git/
我们这里暂定划分3个应用。
- 一个是权限管理系统
- 一个是教学管理系统
- 一个是客户管理系统
根据三个系统模块,我自己画了一个不是很专业的思维导图,咱就先这么将就着看吧,根据这个思维导图来一步一步实现每一个模块。
1.权限管理系统
权限管理系统是客户关系系统中最重要,也是最难理解的部分,权限管理里面涉及到的是系统使用用户的不同权限分配,不同的权限也就决定了用户能够访问到不同的信息。
如:我们的页面中会设计到客户的信息,学生的信息,课程信息,报名信息和缴费信息;我们项目使用的用户每个人都有不同的身份,如,销售、老师、学生、当然还有公司的领导啦。
那么这些角色对应的应权限应该是不同的,你总不能让学生可以访问用户信息,还能修改编辑用户信息把,所以权限的分配在这个项目中至关重要。
哪些表又是与权限相关的呢?
from django.db import models class Userinfo(models.Model): '''存储所有讲师\销售人员\学员 用户信息''' pass class Role(models.Model): '''角色信息''' pass class Permission(models.Model) """存储权限信息表""" pass class Menu(models.Model): """存储菜单展示信息""" pass
权限管理在任意的用户关系管理系统中,应该都会涉及到的一项业务。所以其实我们可以将这一部分业务单独拿出来做成一个组件,将来我们可以把这个组件嵌套在任一与权限分配相关的客户关系管理系统中,专业词也就是rbac(Role-Based Access Control),基于角色的权限访问控制。
RBAC简介
基于角色的权限访问控制(Role-Based Access Control)作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。
在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。
角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系可以建立起来以囊括更广泛的客观情况。
rbac的三个安全原则:最小权限原则,责任分离原则和数据抽象原则。
- 最小权限原则之所以被RBAC所支持,是因为RBAC可以将其角色配置成其完成任务所需要的最小的权限集。
- 责任分离原则可以通过调用相互独立互斥的角色来共同完成敏感的任务而体现,比如要求一个计帐员和财务管理员共参与同一过帐。
- 数据抽象可以通过权限的抽象来体现,如财务操作用借款、存款等抽象权限,而不用操作系统提供的典型的读、写、执行权限。然而这些原则必须通过RBAC各部件的详细配置才能得以体现。
RBAC的关注点在于Role和User, Permission(允许/权限)的关系。称为User assignment(UA)和Permission assignment(PA).关系的左右两边都是Many-to-Many关系。就是user可以有多个role,role可以包括多个user。
这里我就直接展示我们设计的简单的rbac相关表,其中增加了一张menu表,也就是菜单权限,不同的用户有不同的权限,当然也就应该根据权限展示对应的菜单。
我们先新建一个rbac应用
rbac应用下的models文件中,建立如下的数据表模型
1 from django.db import models 2 3 # Create your models here. 4 5 from django.db import models 6 from django.contrib.auth.models import AbstractUser 7 8 # Create your models here. 9 10 # 身份分类 11 role_choices = ( 12 ("1", "董事"), 13 ("2", "CEO"), 14 ("3", "销售"), 15 ("4", "网咨"), 16 ("5", "老师"), 17 ("6", "班主任"), 18 ) 19 20 # 扩展的用户表 21 class UserInfo(AbstractUser): 22 """用户信息表:老师,助教,销售,班主任""" 23 id = models.AutoField(primary_key=True) 24 gender_type = (("male", "男"), ("female", "女")) 25 gender = models.CharField(choices=gender_type, null=True, max_length=12) 26 phone = models.CharField(max_length=11, null=True, unique=True) 27 role = models.ManyToManyField("Role") 28 29 def __str__(self): 30 return self.username 31 32 33 # 身份表 34 class Role(models.Model): 35 title = models.CharField("职位", choices=role_choices, max_length=32) 36 permission = models.ManyToManyField("Permission") 37 38 def __str__(self): 39 return self.title 40 41 42 # 权限表 43 class Permission(models.Model): 44 name = models.CharField(max_length=32, verbose_name=u'权限名') 45 url = models.CharField( 46 max_length=300, 47 verbose_name=u'权限url地址', 48 null=True, 49 blank=True, 50 help_text=u'是否给菜单设置一个url地址' 51 ) 52 icon = models.CharField( 53 max_length=32, 54 verbose_name='权限图标', 55 null=True, 56 blank=True 57 ) 58 # 指定属于哪个父级权限 59 parent = models.ForeignKey( 60 'self', 61 verbose_name=u'父级权限', 62 null=True, 63 blank=True, 64 help_text=u'如果添加的是子权限,请选择父权限' 65 ) 66 67 # 指定属于哪个menu 68 menu = models.ForeignKey(to="Menu",verbose_name=u'对应菜单',blank=True,null=True) 69 70 def __str__(self): 71 return "{parent}{name}".format(name=self.name, parent="%s-->" % self.parent.name if self.parent else '') 72 73 class Meta: 74 verbose_name = u"权限表" 75 verbose_name_plural = u"权限表" 76 ordering = ["id"] 77 78 # 菜单表 79 class Menu(models.Model): 80 title = models.CharField(max_length=32, verbose_name=u'菜单名') 81 # 菜单显示图标 82 icon = models.CharField( 83 max_length=32, 84 verbose_name='菜单图标', 85 null=True, 86 blank=True 87 ) 88 # 指定属于哪个父级菜单 89 parent = models.ForeignKey( 90 'self', 91 verbose_name=u'父级菜单', 92 null=True, 93 blank=True, 94 help_text=u'如果添加的是子菜单,请选择父菜单' 95 ) 96 97 priority = models.IntegerField( 98 verbose_name=u'显示优先级', 99 null=True, 100 blank=True, 101 help_text=u'菜单的显示顺序,优先级越小显示越靠前' 102 ) 103 104 def __str__(self): 105 return "{parent}{title}".format(title=self.title, parent="%s-->" % self.parent.title if self.parent else '') 106 107 class Meta: 108 verbose_name = u"菜单表" 109 verbose_name_plural = u"菜单表" 110 ordering = ["priority","id"] # 根据优先级和id来排序
这里需要注意的几点:
- 新建的应用,注意在项目settings中配置
- 在这个表结构中,我们使用的UserInfo表继承的是django自带的user表,这样我们可以使用django提供给我们的auth认证模块。
如果具体知识有遗忘,可以点这里复习 Django框架—auth认证模块
- 注意使用自己扩招的user表后,切记在settings中配置,告诉django的auth系统要使用你扩展的表,不然会报错
auth.User.groups: (fields.E304) Reverse accessor for 'User.groups' clashes with reverse accessor for 'UserInfo.groups'. HINT: Add or change a related_name argument to the definition for 'User.groups' or 'UserInfo.groups'.
settings中配置
AUTH_USER_MODEL = 'rbac.UserInfo' # 告诉django使用自己扩展的UserInfo表
2.教学管理系统
权限系统订好了,因为是培训学习结构,最重要的当然少不了对于教学管理的业务,教学管理的业务是针对公司具体业务的方向决定的,如果你的公司业务是别的,比如说产品销售,那么设计到的就是订单系统了。
这里我们要写的就是教学管理系统,教学管理系统设计到的就是对于校区的区分,不同的校区又对应有不同的教学课程,每类课程后又涉及到不同的班级等等信息的展示或修改。
而针对用户,涉及到老师,学生,班主任等角色的管理;
- 一个班级中需要有授课老师,授课老师决定课程进度,以及每一节课程的内容课后作业批改,学生成绩考察等。
- 班主任负责班级的日常纪律,学生考勤统计,学院表现信息的录入等等。
- 学生用户使用系统,应该能查看自己的课程大纲,提交每一天课后作业,查看自己的考试成绩等等。
教学系统设计到的表:
class cumpass(models.Model): '''存储所有校区''' pass class ClassList(models.Model): '''存储班级信息''' pass class Course(models.Model): '''存储所开设课程的信息''' pass class CourseRecord(models.Model): '''存储各班级的上课记录''' pass class StudyRecord(models.Model): '''存储所有学员的详细的学习成绩情况''' pass
套路同样来一套,新建一个应用education,应用下的models下详细表结构设计
from django.db import models # Create your models here. # 课程选择 course_choices = ( ('Linux', 'Linux高级'), ('PythonFullStack', 'Python高级全栈开发'), ('BigData', '大数据开发'), ) # 班级类型 class_type_choices = (('fulltime', '脱产班',), ('online', '网络班'), ('weekend', '周末班',),) # 分数分类 score_choices = ((100, 'A+'), (90, 'A'), (85, 'B+'), (80, 'B'), (70, 'B-'), (60, 'C+'), (50, 'C'), (40, 'C-'), (0, ' D'), (-1, 'N/A'), (-100, 'COPY'), (-1000, 'FAIL'),) # 记录选项 record_choices = ( ('checked', "已签到"), ('vacate', "请假"), ('late', "迟到"), ('absence', "缺勤"), ('leave_early', "早退"), ) # 校区表 class Campuses(models.Model): """ 校区表 """ name = models.CharField(verbose_name='校区', max_length=64) address = models.CharField(verbose_name='详细地址', max_length=512, blank=True, null=True) def __str__(self): return self.name # 班级表 class ClassList(models.Model): """ 班级表 """ course = models.CharField("课程名称", max_length=64, choices=course_choices) semester = models.IntegerField("学期") # python20期等 campuses = models.ForeignKey('Campuses', verbose_name="校区", on_delete=models.CASCADE) price = models.IntegerField("学费", default=10000) memo = models.CharField('说明', blank=True, null=True, max_length=100) start_date = models.DateField("开班日期") graduate_date = models.DateField("结业日期", blank=True, null=True) # 不一定什么时候结业,哈哈,所以可为空 # contract = models.ForeignKey('ContractTemplate', verbose_name="选择合同模版", blank=True, null=True,on_delete=models.CASCADE) teachers = models.ManyToManyField('rbac.UserInfo', verbose_name="老师") # 对了,还有一点,如果你用的django2版本的,那么外键字段都需要自行写上on_delete=models.CASCADE class_type = models.CharField(choices=class_type_choices, max_length=64, verbose_name='班额及类型', blank=True, null=True) class Meta: unique_together = ("course", "semester", 'campuses') def __str__(self): return "{}{}({})".format(self.get_course_display(), self.semester, self.campuses) class Student(models.Model): """ 学生表(已报名) """ customer = models.OneToOneField(verbose_name='客户信息', to='customer.Customer', on_delete=models.CASCADE, null=True,blank=True) class_list = models.ManyToManyField(verbose_name="已报班级", to='ClassList', blank=True, related_name="students") emergency_contract = models.CharField(max_length=32, blank=True, null=True, verbose_name='紧急联系人') # 学员毕业就业后的相关信息字段,默认为空 company = models.CharField(verbose_name='公司', max_length=128, blank=True, null=True) date = models.DateField(verbose_name='入职时间', help_text='格式yyyy-mm-dd', blank=True, null=True) location = models.CharField(max_length=64, verbose_name='所在区域', blank=True, null=True) position = models.CharField(verbose_name='岗位', max_length=64, blank=True, null=True) salary = models.IntegerField(verbose_name='薪资', blank=True, null=True) welfare = models.CharField(verbose_name='福利', max_length=256, blank=True, null=True) memo = models.CharField(verbose_name='备注', max_length=256, blank=True, null=True) def __str__(self): return self.customer.qq_name class ClassStudyRecord(models.Model): """ 上课记录表 (班级记录) """ class_obj = models.ForeignKey(verbose_name="班级", to="ClassList", on_delete=models.CASCADE) day_num = models.IntegerField(verbose_name="节次", help_text=u"此处填写第几节课或第几天课程...,必须为数字") teacher = models.ForeignKey(verbose_name="讲师", to='rbac.UserInfo', on_delete=models.CASCADE) date = models.DateField(verbose_name="上课日期") course_title = models.CharField(verbose_name='本节课程标题', max_length=64, blank=True, null=True) course_memo = models.TextField(verbose_name='本节课程内容概要', blank=True, null=True) has_homework = models.BooleanField(default=False, verbose_name="本节有作业", blank=True) homework_title = models.CharField(verbose_name='本节作业标题', max_length=64, blank=True, null=True) homework_memo = models.TextField(verbose_name='作业描述', max_length=500, blank=True, null=True) exam = models.TextField(verbose_name='得分点', max_length=300, blank=True, null=True) def __str__(self): return "{0} day{1}".format(self.class_obj, self.day_num) class StudentStudyRecord(models.Model): ''' 学生学习记录 ''' student = models.ForeignKey(verbose_name="学员", to='Student', on_delete=models.CASCADE) class_study_record = models.ForeignKey(verbose_name="第几天课程", to="ClassStudyRecord", on_delete=models.CASCADE) record = models.CharField("上课纪录", choices=record_choices, default="checked", max_length=64) score = models.IntegerField("本节成绩", choices=score_choices, default=-1) homework = models.FileField(verbose_name='作业文件', blank=True, null=True, default=None) stu_memo = models.TextField(verbose_name='学员备注', blank=True, null=True) date = models.DateTimeField(verbose_name='提交作业日期', auto_now_add=True) homework_note = models.CharField(verbose_name='作业评语', max_length=255, blank=True, null=True) note = models.CharField(verbose_name="备注", max_length=255, blank=True, null=True) def __str__(self): return "{0}-{1}".format(self.class_study_record, self.student) class Meta: unique_together = ["student", "class_study_record"]
3.客户管理系统
客户管理系统,供销售人员使用,具体业务是针对客户信息的查看,客户信息的增加和修改。客户开始都是位于客户管理的公共信息上,每个销售可以根据自己的业务情况,将共有客户转化为自己的私有客户进行跟进,跟进成功后,通过报名表来登记客户报名的情况。
在这个过程中,如果客户a被销售1跟进,那么这段时间内,客户a就属于销售1的私有客户,其他客户就无法获取客户a的信息以及跟进。
当然,销售1也可以自行放弃客户a的跟进,比如说自己感觉撩不动啊,或者客户a太婆妈啊等等,这个时候销售可以将客户a又推回公户,这个时候,其他有能力或者想法的销售就可以紧接着去跟进客户a。
客户系统表model创建
既然我们决定分出客户管理系统,那么对应的表也应该放在客户管理应用下的models中
客户系统涉及到的表:
class Customer(models.Model): '''存储所有客户信息''' pass class ConsultRecord(models.Model): '''存储客户的后续跟进信息''' pass class Enrollment(models.Model): '''存储已报名学员的信息''' pass
首先我们新建一个customer应用,用来处理与customer的所有业务数据。
然后在应用的models文件中创建表结构,具体的表结构设计如下,表结构设计需要消耗很长时间的考虑和分析,这里直接展示,不再详述
from django.db import models from multiselectfield import MultiSelectField # 安装:pip install django-multiselectfield,针对choices多选用的 # Create your models here. # 客户来源分类 source_type = (('qq', "qq群"), ('referral', "内部转介绍"), ('website', "官方网站"), ('baidu_ads', "百度推广"), ('office_direct', "直接上门"), ('WoM', "口碑"), ('public_class', "公开课"), ('website_luffy', "路飞官网"), ('others', "其它"),) # 报名状态分类 enroll_status_choices = (('signed', "已报名"), ('unregistered', "未报名"), ('studying', '学习中'), ('paid_in_full', "学费已交齐")) # 客户意向分类 seek_status_choices = ( ('A', '近期无报名计划'), ('B', '1个月内报名'), ('C', '2周内报名'), ('D', '1周内报名'), ('E', '定金'), ('F', '到班'), ('G', '全款'), ('H', '无效'), ) # 咨询班级类型 class_type_choices = (('fulltime', '脱产班',), ('online', '网络班'), ('weekend', '周末班',),) # 咨询课程选择 course_choices = ( ('Linux', 'Linux高级'), ('PythonFullStack', 'Python高级全栈开发'), ('BigData', '大数据开发'), ) # 客户信息表 class Customer(models.Model): """ 客户表(最开始的时候大家都是客户,销售就不停的撩你,你还没交钱就是个客户) """ qq = models.CharField(verbose_name='QQ', max_length=64, unique=True, help_text='QQ号必须唯一') qq_name = models.CharField('QQ昵称', max_length=64, blank=True, null=True) name = models.CharField('姓名', default="潜在客户", max_length=32, blank=True, null=True, help_text='学员报名后,请改为真实姓名') gender_type = (('male', '男'), ('female', '女')) gender = models.CharField("性别", choices=gender_type, max_length=16, default='male', blank=True, null=True) # 存的是male或者female,字符串 birthday = models.DateField('出生日期', default=None, help_text="格式yyyy-mm-dd", blank=True, null=True) phone = models.CharField('手机号', blank=True, null=True, max_length=32) # phone = models.CharField('手机号', blank=True, null=True) source = models.CharField('客户来源', max_length=64, choices=source_type, default='qq') introduce_from = models.ForeignKey('self', verbose_name="转介绍自学员", blank=True, null=True) # self指的就是自己这个表,和下面写法是一样的效果 # introduce_from = models.ForeignKey('Customer', verbose_name="转介绍自学员", blank=True, null=True,on_delete=models.CASCADE) course = MultiSelectField("咨询课程", choices=course_choices, blank=True, null=True) # 多选,并且存成一个列表的格式 # course = models.CharField("咨询课程", choices=course_choices) #如果你不想用上面的多选功能,可以使用Charfield来存 class_type = models.CharField("咨询班级类型", max_length=64, choices=class_type_choices, default='fulltime', blank=True, null=True) customer_note = models.TextField("客户备注", blank=True, null=True, ) status = models.CharField("状态", choices=enroll_status_choices, max_length=64, default="unregistered", help_text="选择客户此时的状态") # help_text这种参数基本都是针对admin应用里面用的 date = models.DateTimeField("咨询日期", ) last_consult_date = models.DateField("最后跟进日期", auto_now_add=True) # 考核销售的跟进情况,如果多天没有跟进,会影响销售的绩效等 next_date = models.DateField("预计再次跟进时间", blank=True, null=True) # 销售自己大概记录一下自己下一次会什么时候跟进,也没啥用 # 用户表中存放的是自己公司的所有员工。 consultant = models.ForeignKey('rbac.UserInfo', verbose_name="销售", blank=True, null=True) def __str__(self): return self.qq_name # 主要__str__最好是个字符串昂,不然你会遇到很多的坑,还有我们返回的这两个字段填写数据的时候必须写上数据,必然相加会报错,null类型和str类型不能相加等错误信息。 # 跟进记录表 class ConsultRecord(models.Model): """ 跟进记录表 """ customer = models.ForeignKey('Customer', verbose_name="咨询客户") status = models.CharField("跟进状态", max_length=8, choices=seek_status_choices, help_text="选择客户此时的状态") note = models.TextField(verbose_name="跟进内容...") consultant = models.ForeignKey("rbac.UserInfo", verbose_name="跟进人", related_name='records') date = models.DateTimeField("跟进日期", auto_now_add=True) delete_status = models.BooleanField(verbose_name='删除状态', default=False) # 登记报名表 class Enrollment(models.Model): """报名表""" customer = models.ForeignKey('Customer', verbose_name='客户名称') why = models.TextField("为什么报名", max_length=1024, default=None, blank=True, null=True) expectation = models.TextField("学习期望", max_length=1024, blank=True, null=True) enrolled_date = models.DateTimeField(verbose_name="报名日期") memo = models.TextField('备注', blank=True, null=True) delete_status = models.BooleanField(verbose_name='删除状态', default=False) school = models.ForeignKey('education.Campuses') # 校区表 enrollment_class = models.ForeignKey("education.ClassList", verbose_name="所报班级") class Meta: unique_together = ('enrollment_class', 'customer')
注意
上述表结构分别在不同应用中生成,所以在使用外键关联的时候,需要注意外键字段中关联表名,需要带上应用名,这样django才能够找到对应的表进行关联。
如:销售字段关联rbac中的UserInfo表写法
consultant = models.ForeignKey("rbac.UserInfo", verbose_name="跟进人", related_name='records')
4.数据库同步
表结构都设计完成了,最后就差一步同步数据库了,正常项目中我们应该配置我公司使用的数据库,因为这里是知识整理分享,所以我们使用django自带的db.sqlite3.
如果我们想使用mysql也可以,具体配置参考这里数据库的配置:django框架—模型层ORM
同步数据库
在django项目环境下执行makemigrations和migrate