起步
在文華學院讀大二時,接觸到一名華中科技大學的學長,他當時在搞少兒編程教育的創業項目,他邀請我作爲兼職講師加入到他們的團隊裏,我答應了。我們的團隊當時還比較小,專攻線下市場,後來隨着規模的擴大,管理上就有點力不從心。我那時正好在學習Django,所以就打算做一套系統,供我們的項目團隊使用,雖然後面離開了這個團隊,系統也一直沒有交付,但還是有所收穫。
需求分析
作爲一個供教育培訓機構使用的CRM系統,前期主要是調研這個系統的需求是什麼,前期梳理表結構如下,後期可以根據市場需要自行擴展:
作爲招生人員(銷售):
- 銷售註冊,登錄系統
- 銷售添加客戶信息,成爲銷售的私戶
- 銷售固定時間跟進客戶
- 客戶報名
- 銷售審覈報名,學生進行繳費
- 銷售將錢交給財務,財務對繳費記錄進行審覈
- 自動修改學生狀態
- 續費通知
作爲班主任:
- 每天創建課程記錄,記錄每天上課的情況
- 再根據課程記錄,生成學生的學習記錄,修改考勤情況
- 調班/退班
- 教學情況統計
作爲講師:
- 查看/申請變更排課日程
- 課時費用結算
作爲學員(少兒嘛,一般是家長看):
- 查看課程表
- 查看學習情況
作爲機構管理者:
- 運營分析報表
- 用戶表
- 客戶表
- 跟進記錄表
- 報名記錄表
- 班級表
- 校區表
- 合同表
- 繳費記錄表
- 課程記錄表
- 學習記錄表
- 部門表
表結構設計
class UserProfile(AbstractBaseUser, PermissionsMixin):
"""
用戶表
"""
username = models.EmailField(
max_length=255,
unique=True,
)
is_staff = models.BooleanField(
_('staff status'),
default=False,
help_text=_('Designates whether the user can log into this admin site.'),
)
is_admin = models.BooleanField(default=False)
name = models.CharField('名字', max_length=32)
department = models.ForeignKey('Department', default=None, blank=True, null=True)
mobile = models.CharField('手機', max_length=32, default=None, blank=True, null=True)
memo = models.TextField('備註', blank=True, null=True, default=None)
date_joined = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['name']
class Meta:
verbose_name = '賬戶信息'
verbose_name_plural = "賬戶信息"
def get_full_name(self):
# The user is identified by their email address
return self.name
def get_short_name(self):
# The user is identified by their email address
return self.username
def __str__(self): # __unicode__ on Python 2
return self.username
def has_perm(self, perm, obj=None):
# "Does the user have a specific permission?"
# Simplest possible answer: Yes, always
if self.is_active and self.is_superuser:
return True
return _user_has_perm(self, perm, obj)
def has_perms(self, perm_list, obj=None):
# "Does the user have a specific permission?"
# Simplest possible answer: Yes, always
for perm in perm_list:
if not self.has_perm(perm, obj):
return False
return True
def has_module_perms(self, app_label):
# "Does the user have permissions to view the app `app_label`?"
# Simplest possible answer: Yes, always
if self.is_active and self.is_superuser:
return True
return _user_has_module_perms(self, app_label)
objects = UserManager()
class Customer(models.Model):
"""
客戶表
"""
qq = models.CharField('QQ', max_length=64, unique=True, help_text='QQ號必須唯一')
qq_name = models.CharField('QQ暱稱', max_length=64, blank=True, null=True)
name = models.CharField('姓名', max_length=32, blank=True, null=True, help_text='學員報名後,請改爲真實姓名')
sex_type = (('male', '男'), ('female', '女'))
sex = models.CharField("性別", choices=sex_type, max_length=16, default='male', blank=True, null=True)
birthday = models.DateField('出生日期', default=None, help_text="格式yyyy-mm-dd", blank=True, null=True)
phone = models.BigIntegerField('手機號', 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)
course = MultiSelectField("諮詢課程", choices=course_choices)
class_type = models.CharField("班級類型", max_length=64, choices=class_type_choices, default='fulltime')
customer_note = models.TextField("客戶備註", blank=True, null=True, )
status = models.CharField("狀態", choices=enroll_status_choices, max_length=64, default="unregistered",
help_text="選擇客戶此時的狀態")
network_consult_note = models.TextField(blank=True, null=True, verbose_name='網絡諮詢師諮詢內容')
date = models.DateTimeField("諮詢日期", auto_now_add=True)
last_consult_date = models.DateField("最後跟進日期", auto_now_add=True)
next_date = models.DateField("預計再次跟進時間", blank=True, null=True)
network_consultant = models.ForeignKey('UserProfile', blank=True, null=True, verbose_name='諮詢師',
related_name='network_consultant')
consultant = models.ForeignKey('UserProfile', verbose_name="銷售", related_name='customers', blank=True, null=True, )
class_list = models.ManyToManyField('ClassList', verbose_name="意向班級",blank=True )
class ConsultRecord(models.Model):
"""
跟進記錄表
"""
customer = models.ForeignKey('Customer', verbose_name="所諮詢客戶")
note = models.TextField(verbose_name="跟進內容...")
status = models.CharField("跟進狀態", max_length=8, choices=seek_status_choices, help_text="選擇客戶此時的狀態")
consultant = models.ForeignKey("UserProfile", verbose_name="跟進人", related_name='records')
date = models.DateTimeField("跟進日期", auto_now_add=True)
delete_status = models.BooleanField(verbose_name='刪除狀態', default=False)
class Enrollment(models.Model):
"""
報名表
"""
why_us = models.TextField("爲什麼報名", max_length=1024, default=None, blank=True, null=True)
your_expectation = models.TextField("學完想達到的具體期望", max_length=1024, blank=True, null=True)
contract_agreed = models.BooleanField("我已認真閱讀完培訓協議並同意全部協議內容", default=False)
contract_approved = models.BooleanField("審批通過", help_text="在審閱完學員的資料無誤後勾選此項,合同即生效", default=False)
enrolled_date = models.DateTimeField(auto_now_add=True, verbose_name="報名日期")
memo = models.TextField('備註', blank=True, null=True)
delete_status = models.BooleanField(verbose_name='刪除狀態', default=False)
customer = models.ForeignKey('Customer', verbose_name='客戶名稱')
school = models.ForeignKey('Campuses',verbose_name='校區')
enrolment_class = models.ForeignKey("ClassList", verbose_name="所報班級")
class Meta:
unique_together = ('enrolment_class', 'customer')
class ClassList(models.Model):
"""
班級表
"""
course = models.CharField("課程名稱", max_length=64, choices=course_choices)
semester = models.IntegerField("學期")
campuses = models.ForeignKey('Campuses', verbose_name="校區")
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)
teachers = models.ManyToManyField('UserProfile', verbose_name="老師")
class_type = models.CharField(choices=class_type_choices, max_length=64, verbose_name='班額及類型', blank=True,
null=True)
class Meta:
unique_together = ("course", "semester", 'campuses')
class Campuses(models.Model):
"""
校區表
"""
name = models.CharField(verbose_name='校區', max_length=64)
address = models.CharField(verbose_name='詳細地址', max_length=512, blank=True, null=True)
class ContractTemplate(models.Model):
"""
合同模板表
"""
name = models.CharField("合同名稱", max_length=128, unique=True)
content = models.TextField("合同內容")
date = models.DateField(auto_now=True)
class PaymentRecord(models.Model):
"""
繳費記錄表
"""
pay_type = models.CharField("費用類型", choices=pay_type_choices, max_length=64, default="deposit")
paid_fee = models.IntegerField("費用數額", default=0)
note = models.TextField("備註", blank=True, null=True)
date = models.DateTimeField("交款日期", auto_now_add=True)
course = models.CharField("課程名", choices=course_choices, max_length=64, blank=True, null=True, default='N/A')
class_type = models.CharField("班級類型", choices=class_type_choices, max_length=64, blank=True, null=True,
default='N/A')
enrolment_class = models.ForeignKey('ClassList', verbose_name='所報班級', blank=True, null=True)
customer = models.ForeignKey('Customer', verbose_name="客戶")
consultant = models.ForeignKey('UserProfile', verbose_name="銷售")
delete_status = models.BooleanField(verbose_name='刪除狀態', default=False)
status_choices = (
(1, '未審覈'),
(2, '已審覈'),
)
status = models.IntegerField(verbose_name='審覈', default=1, choices=status_choices)
confirm_date = models.DateTimeField(verbose_name="確認日期", null=True, blank=True)
confirm_user = models.ForeignKey(verbose_name="確認人", to='UserProfile', related_name='confirms', null=True,
blank=True)
class CourseRecord(models.Model):
"""課程記錄表"""
day_num = models.IntegerField("節次", help_text="此處填寫第幾節課或第幾天課程...,必須爲數字")
date = models.DateField(auto_now_add=True, verbose_name="上課日期")
course_title = models.CharField('本節課程標題', max_length=64, blank=True, null=True)
course_memo = models.TextField('本節課程內容', max_length=300, blank=True, null=True)
has_homework = models.BooleanField(default=True, verbose_name="本節有作業")
homework_title = models.CharField('本節作業標題', max_length=64, blank=True, null=True)
homework_memo = models.TextField('作業描述', max_length=500, blank=True, null=True)
scoring_point = models.TextField('得分點', max_length=300, blank=True, null=True)
re_class = models.ForeignKey('ClassList', verbose_name="班級")
teacher = models.ForeignKey('UserProfile', verbose_name="班主任")
class Meta:
unique_together = ('re_class', 'day_num')
class StudyRecord(models.Model):
"""
學習記錄
"""
attendance = models.CharField("考勤", choices=attendance_choices, default="checked", max_length=64)
score = models.IntegerField("本節成績", choices=score_choices, default=-1)
homework_note = models.CharField(max_length=255, verbose_name='作業批語', blank=True, null=True)
date = models.DateTimeField(auto_now_add=True)
note = models.CharField("備註", max_length=255, blank=True, null=True)
homework = models.FileField(verbose_name='作業文件', blank=True, null=True, default=None)
course_record = models.ForeignKey('CourseRecord', verbose_name="某節課程")
student = models.ForeignKey('Customer', verbose_name="學員")
class Meta:
unique_together = ('course_record', 'student')
class Department(models.Model):
"""
部門表
"""
name = models.CharField(max_length=32, verbose_name="部門名稱")
count = models.IntegerField(verbose_name="人數", default=0)
"""
字段選擇元組
"""
course_choices = (('Block', '智能積木'),
('Scratch', 'scratch編程'),
('Car', '智能小車'),
('Python', 'python編程'),
('Html', 'HTML網頁編程'),
('Aircraft ', '無人機'),)
class_type_choices = (('fri', '週五班',),
('sat', '週六班'),
('sun', '週日班',),)
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', '無效'),)
pay_type_choices = (('deposit', "訂金/報名費"),
('tuition', "學費"),
('transfer', "轉班"),
('dropout', "退學"),
('refund', "退款"),)
attendance_choices = (('checked', "已簽到"),
('vacate', "請假"),
('late', "遲到"),
('absence', "缺勤"),
('leave_early', "早退"),)
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'),)
class UserManager(BaseUserManager):
"""
系統配置相關
"""
use_in_migrations = True
def _create_user(self, username, password, **extra_fields):
"""
Creates and saves a User with the given username, email and password.
"""
if not username:
raise ValueError('The given username must be set')
username = self.normalize_email(username)
username = self.model.normalize_username(username)
user = self.model(username=username, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(username, password, **extra_fields)
def create_superuser(self, username, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(username, password, **extra_fields)
# A few helper functions for common logic between User and AnonymousUser.
def _user_get_all_permissions(user, obj):
permissions = set()
for backend in auth.get_backends():
if hasattr(backend, "get_all_permissions"):
permissions.update(backend.get_all_permissions(user, obj))
return permissions
def _user_has_perm(user, perm, obj):
"""
A backend can raise `PermissionDenied` to short-circuit permission checking.
"""
for backend in auth.get_backends():
if not hasattr(backend, 'has_perm'):
continue
try:
if backend.has_perm(user, perm, obj):
return True
except PermissionDenied:
return False
return False
def _user_has_module_perms(user, app_label):
"""
A backend can raise `PermissionDenied` to short-circuit permission checking.
"""
for backend in auth.get_backends():
if not hasattr(backend, 'has_module_perms'):
continue
try:
if backend.has_module_perms(user, app_label):
return True
except PermissionDenied:
return False
return False