基础业务处理
# CRM介绍
该crm系统以教育机构为背景. 主要为 销售部、运营部、教质部 提供管理平台. 通过系统来约束员工的行为!
官方点: 随着公司规模的扩展, CRM系统对公司员工的业务信息量化以及信息化建设越来越重要!
# 项目背景
crm系统为不同角色(销售、运营、教质)的用户提供了不同的功能
■ 销售部
客户分为公户和私户
- 公户,公共客户(通过广告、转介绍、上门咨询..等渠道获得的客户)
- 私户,销售自己的客户
1> "公户转换成私户"
所有销售都能看到公户的信息,想要将公户转换成私户,需要进行申请
申请将公户放到自己的私户里 (Ps:这样哪怕其他人临门一脚,把客户抢了促成了这一单,业绩也会算作是自己的!)
2> "私户分配限制"
每个销售的私户个数有限制,一般 <=150个客户
只有当自己的私户个数小于规定客户数时,才能去公户里转换客户成自己的私户!
所以,当销售跟某个客户聊天跟进一段时间后,觉得没戏,就可以将该客户 踢到 公户中,这样就能申请公户到自己私户中了.
3> "跟进记录"
销售需要填写跟进记录,一方面是为了方便隔一段时间后继续跟进客户;另一方面是放弃后,踢到公户中,能让其他想转换的销售了解信息.
4> "添加入班申请"
当客户转化成功之后,要添加入班申请且缴费信息,最终由财务审核入班
2
3
4
5
6
7
8
9
10
11
12
13
14
■ 运营部
在该系统中运营部的主要工作是,录入客户信息到公户并对于客户进行跟踪!
在真实的工作场景中,运营部还需要进行数据分析等工作.
2
■ 教质部
- 日常学员考勤及上课记录
- 定时对学员进行谈话
- 纪律维护(关乎积分管理)
- 可由班主任可以发起转班以及留级申请(定期考核不通过就得留级)
2
3
4
# 项目开发概览
先对项目的开发有个概览 (´・Д・)」
- 基础业务处理
- 校区管理(北京、上海、深圳、广州)
- 部门管理(销售、运营、教质)
- 用户管理
- 课程管理
- 开班管理
- 客户管理
- 公户
- 私户
- 缴费和报名申请
- 学员管理
- 学生管理
- 考勤
- 谈话记录
- 积分
- 应用rbac组件,进行权限的控制
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS: 理论上,学生可登录系统查看自己的个人信息.. 该系统中暂不开发该功能.
# 准备工作
ψ(`∇´)ψ
■ 创建项目
新建一个python项目dc_crm + 利用pycharm构建虚拟环境
pip install Django==3.2 -i https://pypi.tuna.tsinghua.edu.cn/simple
django-admin startproject dc_crm .
python manage.py startapp web
- 项目根目录下创建apps文件,将web放到里面
- 记得将 apps/web/apps.py 里的 name = 'web' 改为 name = 'apps.web'
- 注册app 'apps.web.apps.WebConfig',
将我们现目前写好的RBAC组件也放到apps目录下,并注册app!!
- ★ 记得将RBAC组件下的 迁移文件 删除!!
将我们现目前写好的RBAC组件也放到apps目录下,并注册app!!
进行Django项目快速启动的配置
进行数据库迁移
- 一开始web中没有表
- stark组件中也没有表
- rbac组件数据库迁移会生成 一级菜单表、角色表、权限表、自动生成权限和角色的第三张表 --> 共4张表!
rbac组件里的UserInfo用户表设置了abstract = True是不会生成表的!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
■ 配置总路由
# -- dc_crm/urls.py
from django.contrib import admin
from django.urls import path
from apps.stark.service.v1 import site
urlpatterns = [
path('admin/', admin.site.urls),
path('stark/', site.urls),
]
2
3
4
5
6
7
8
9
我们计划先用stark组件来完成业务开发, 再用rbac组件来完成权限控制!!
■ 在web这个业务相关app里创建stark.py文件
# 校区管理|部门管理
利用stark组件,对web这个app里的School校区表和Department部门表 这两张 单表 快速完成CURD的编写!!
Step1: 在web这个app里创建School校区表、Department部门表, 并进行数据库迁移
from django.db import models
class School(models.Model):
""" 校区表 如: 北京昌平校区 上海浦东校区 深圳南山校区 """
title = models.CharField(verbose_name='校区名称', max_length=32)
def __str__(self):
return self.title
class Department(models.Model):
""" 部门表 """
title = models.CharField(verbose_name='部门名称', max_length=16)
def __str__(self):
return self.title
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Step2: 在web这个app的stark.py文件里编写以下内容!
from apps.stark.service.v1 import site, StarkHandler
from apps.web import models
class SchoolHandler(StarkHandler):
list_display = ["title"]
site.register(models.School, SchoolHandler)
class DepartmentHandler(StarkHandler):
list_display = ['title']
site.register(models.Department, DepartmentHandler)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
至此, School表的CURD完成!! 访问路由: http://127.0.0.1:8000/stark/web/department/list/
至此, Department表的CURD完成!! 访问路由: http://127.0.0.1:8000/stark/web/school/list/
其他:
1.注意 多app的模版查找顺序
2.在原来的stark组件基础上, 增加了 默认添加 "操作"一列的代码!(即 编辑|删除 放在了一起
2
3
# 代码拆分
按照下面的代码结构, 更加的清晰!!
# 用户管理
(*≧ω≦)
■ 用户添加和用户编辑需要定义不同的ModelForm!
1> 新增时,确认密码 -- 新增/添加页面新增一个确认密码的字段.
2> 编辑时,不应该对密码进行操作 -- 编辑页面没有密码字段.
■ 再定义一个ModelForm用于重置密码!
★★★ 明确两点:
1. 查看了下源码,Form组件没有save方法! ModelForm组件有save方法!!
2. - 试验了下,ModelForm更新时,使用save,无需所有字段,前端表单中填的字段有一个是ORM表中的就行;
(多的字段是ORM表中没有的字段也无伤大雅
- 试验了下,ModelForm新增时,使用save,除ORM表中允许为空和默认的,其余字段在前端表单中必须都有,少一个都不行!!
(但多了却没事Hhh,比如多了个重复密码的额外字段.
新增和更新,有多的字段都没事!新增不能少,更新可以少.
★ 无论是新增还是更新,其内部的本质是 将form.instance进行save!!不是将form.cleaned_data来进行save哦!
经过内部处理后,form.instance里面有的字段只会是ORM表中的字段!!
PS:若你头铁,使用 self.model_class.objects.create(**form.cleaned_data) 来进行新增的话,必须确保一个不多一个不少!!
╮( ̄▽ ̄"")╭ 再多说一点,前后端分离的drf组件,使用save新增时,应该是一个不多一个不少!!多了,save前pop;少了,就给save传参!!
(¯﹃¯)因为其内部是用 校验成功后的数据进行的save!!
2
3
4
5
6
7
8
9
10
11
12
# CURD
(・_・;
关键代码如下:
其他:
1. 在原来的stark组件基础上,
重写get_model_form_class 实现 新增使用1号ModelForm, 编辑使用2号ModelForm..
2. 在原来的stark组件基础上,
多对对字段 使用ModelForm内 对应的是 ModelMultipleChoiceField 表单字段对象..
ModelMultipleChoiceField默认使用的插件是 SelectMultiple..
我在__init__方法里,通过类型判断,将ModelMultipleChoiceField表单字段对象的插件 改成 CheckboxSelectMultiple..
这样, 表中的多对多字段就无需自己 在class Meta中通过 widgets 指定了..
而且通过 __init__ 的方法,还可以提取父类,其它地方也可以用.
3. 在原来的stark组件基础上, 实现密码隐藏 以及 存储密码时是经过md5加密的!!
4. modelform验证过程,使用clean方法!! 特别注意,字段自身校验不通过,self.cleaned_data中不会有该字段!!源码可考究!
2
3
4
5
6
7
8
9
10
11
# 重置密码
╮( ̄▽ ̄"")╭
用户管理页面新增一列"重置密码"、在原有的CURD的url上新增"重置密码"的url, 编写对应的视图函数和form表单..
# 使用Form组件
重置密码页面 只有两个字段 "密码"、"确认密码"! 也无需填充原来的密码的值, 展示页面 也跟ORM数据库的字段没啥关系!
所以我们 使用 Form组件 来实现!!
关键代码如下:
其他:
1. 在原来的stark组件基础上,对标StarkModelForm添加了一个父类 StarkForm..
2
# 使用ModelForm组件
在重置密码的页面, 用户名字段是不能编辑的. - 用 readonly 和 initial 来实现的!!
结合 rbac部分 - 菜单和权限管理.md - 用户管理小节 的笔记 进行理解!!
read_only的详细解释参考: rbac组件中菜单和权限管理.md中的用户管理里的内容!!
关键代码如下:
Ps;截图中有一处代码不规范!
form = ResetPasswordForm(instance=userinfo_object, data=request.POST, initial={'name': userinfo_object.name})
应改为:
form = ResetPasswordForm(instance=userinfo_object, data=request.POST)
因为,官方文档是这么说的:
`initial` 值在验证中 *不* 用作“后备”数据, 如果某个字段的值没有给出. `initial` 值 *仅* 用于初始表格显示.
2
3
4
5
6
★ 特别注意!! 一点小思考:
上述代码,看似name字段设置了read_only在页面上不能进行编辑!!
实则,右键检查 可以改该input框的value值间接达到修改效果!! 修改后会同修改的密码一起提交到后端save保存!!
但从该应用场景来说,能 重置密码的人 权限都很高!!没必要通过这种方式来修改name字段的值!!
那严谨点,如何编写呢?在该示例中!
read_only + 在save之前将name字段的值强行赋值一遍.如何原有的name字段值呢?从userinfo_object中取.
★ PS:若使用的是stark组件默认的更新视图函数的逻辑.
1.第一步需要改一下源码,让modelform在实例化时,可以传递initial参数!(我没有想到好的办法来实现,暂且搁置了!!)
2.第二步就重写save方法:
因为该方法有形参request嘛,通过request取url中的pk值,进而拿到对象!哈哈哈哈.曲线救国!(*≧ω≦)
■ 思考一下,若新增的路由是这样的 "add/<int:customer_id>/",
页面显示的默认值还需要从*kwargs中取!ModelForm表单实例化时还需要传递initial参数,不好修改源码!!
(・_・; 其实想来想去最好的就是重写该路由该视图函数!!简单快捷!没错就这么干!! 不改源码啦!
2
3
4
5
6
7
8
9
10
11
12
13
14
# 课程管理
对Course课程表这张 单表 快速完成CURD的编写!!
Step1: 在web这个app里创建Course课程表并进行数据库迁移
from django.db import models
class Course(models.Model):
""" 课程表 如:Linux基础、Linux架构师、Python自动化、Python全栈 """
name = models.CharField(verbose_name='课程名称', max_length=32)
def __str__(self):
return self.name
2
3
4
5
6
7
8
9
Step2: 在web这个app的stark.py文件里编写以下内容!
from apps.stark.service.v1 import site
from apps.web import models
from apps.web.views.depart import DepartmentHandler
from apps.web.views.school import SchoolHandler
from apps.web.views.userinfo import UserInfoHandler
from apps.web.views.course import CourseHandler
site.register(models.School, SchoolHandler)
site.register(models.Department, DepartmentHandler)
site.register(models.UserInfo, UserInfoHandler)
site.register(models.Course, CourseHandler)
2
3
4
5
6
7
8
9
10
11
Step3: 在web这个app的views/course.py文件里编写以下内容!
from apps.stark.service.v1 import StarkHandler
class CourseHandler(StarkHandler):
list_display = ["name"]
2
3
4
5
至此, Course课程表的CURD完成!! 访问路由: http://127.0.0.1:8000/stark/web/course/list/
# 班级管理
(*≧ω≦)
# 数据表设计
先来看看数据表是怎么设计的!
关键代码如下:
class ClassList(models.Model):
""" 班级表 如: 上海浦东校区 Python全栈 5期 10000 2024-01-01 2024-06-10 """
school = models.ForeignKey(verbose_name='校区', to='School', on_delete=models.CASCADE)
course = models.ForeignKey(verbose_name='课程名称', to='Course', on_delete=models.CASCADE)
semester = models.PositiveIntegerField(verbose_name="班级(期)")
price = models.PositiveIntegerField(verbose_name="学费")
start_date = models.DateField(verbose_name="开班日期")
graduate_date = models.DateField(verbose_name="结业日期", null=True, blank=True)
class_teacher = models.ForeignKey(
verbose_name='班主任',
to='UserInfo',
related_name='classes',
on_delete=models.CASCADE,
)
tech_teachers = models.ManyToManyField(
verbose_name='任课老师',
to='UserInfo',
related_name='teach_classes',
blank=True,
)
memo = models.TextField(verbose_name='说明', blank=True, null=True)
def __str__(self):
return f"{self.course.name}({self.semester}期)"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PositiveIntegerField 正整数;
结业日期不确定, 所有可为空;
当表中有多个字段都关联了同一张表, 设置related_name 用于反向生成!
# 表格数据展示
(*≧ω≦)
其他:
1. 在原来的stark组件基础上,添加了处理时间日期格式的闭包
2. 在原来的stark组件基础上,添加了处理多对多文本信息的闭包
2
3
# 新增和编辑
(*≧ω≦)
先来看看效果
关键代码如下:
★ 注意: 我们规定在新增和编辑时, 班主任得是教质部的人, 任课老师得是教学部的人!
userinfo用户表,有个外键字段是关联depart部门表的!!(eg: 教学部、教质部
classlist班级表, 班主任字段n:1关联用户表、任课老师字段n:n关联用户表!!
若不作任何处理,在新增和编辑时,班主任和任课老师可供选择的是所有的用户!! 这是不符合我们的想法的.
■ 解决方案1: 用limit_choices_to属性!(直接添加即可,无需数据库迁移)
class_teacher = models.ForeignKey(
verbose_name='班主任', to='UserInfo',
related_name='classes', limit_choices_to={'depart__title': '教质部'})
tech_teachers = models.ManyToManyField(
verbose_name='任课老师', to='UserInfo',
related_name='teach_classes', blank=True, limit_choices_to={'depart__title': '教质部'})
■ 解决方案2: 见上方截图!在__init__中处理(推荐!!)
2
3
4
5
6
7
8
9
10
11
12
13
14
# ■ 需求:所教授课程
在班级管理页面,我们可以看到 该课程有哪些老师教!! 那么在用户管理界面, 我们想看到, 教师部的用户教了哪些课程!!
要实现这个需求, 关键在于理解 多对多的反向查找!!
先来看看效果
首先,我先写了个脚本看能不能拿到自己想要的数据!
import os
import sys
import django
base_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(base_dir)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dc_crm.settings')
django.setup()
if __name__ == '__main__':
from apps.web import models
obj = models.UserInfo.objects.filter(nickname="张开").first()
# 多对多反向查询张开教了哪些课程! 运用了related_name
# - <QuerySet [('Python自动化',), ('Linux基础',)]>
print(obj.teach_classes.values_list("course__name"))
# - <QuerySet [{'course__name': 'Python自动化'}, {'course__name': 'Linux基础'}]>
print(obj.teach_classes.values("course__name"))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
接着, 在UserInfoHandler我是这样搞的!! 关键代码如下: (重点关注display_teach_class 方法!!
class UserInfoHandler(StarkHandler):
def display_reset_pwd(self, obj=None, is_header=None):
if is_header:
return '重置密码'
name = f"{self.site.namespace}:{self.get_url_name('reset_pwd')}"
reset_url = self.reverse_url(name, pk=obj.pk)
return mark_safe(f"<a href='{reset_url}'>重置密码</a>")
def display_teach_class(self, obj=None, is_header=None):
if is_header:
return '教授课程'
if obj.depart.title != "教学部":
return mark_safe("<span style='color:gray'>无授课资质</span>")
btn_str = '<div style="display: flex;flex-wrap: wrap;width:700px">'
for class_obj in obj.teach_classes.all().select_related("school", "course"):
text = f"{class_obj.school.title} - {class_obj.course.name} - {class_obj.semester}期"
btn_str += f'''
<button type="button" class="layui-btn layui-btn-sm layui-bg-default"
style="margin:0 10px 10px 0">{text}</button>
'''
btn_str += '</div>'
return mark_safe(btn_str)
list_display = ['nickname', get_choice_text('性别', 'gender'),
'phone', 'email', 'depart', display_reset_pwd, display_teach_class]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25