客户管理
何为公户? 何为私户? 客户表里, 若该记录没有课程顾问, 那么该客户就是公户; 否则就是私户!!
# 客户表
(*≧ω≦)
注意: consultant课程顾问字段/即销售人员.. 设置了属性 limit_choices_to={'depart__title': '销售部'},
那么, 在新增和编辑时, consultant课程顾问字段的选项就只会是 销售部的人!!
class Customer(models.Model):
""" 客户表 """
MAX_PRIVATE_CUSTOMER_COUNT = 150 # 最大私户人数
name = models.CharField(verbose_name='姓名', max_length=32)
qq = models.CharField(verbose_name='联系方式', max_length=64, unique=True, help_text='QQ号/微信/手机号')
status_choices = [
(1, "已报名"),
(2, "未报名")
]
status = models.IntegerField(verbose_name="状态", choices=status_choices, default=2)
gender_choices = ((1, '男'), (2, '女'))
gender = models.SmallIntegerField(verbose_name='性别', choices=gender_choices)
source_choices = [
(1, "qq群"),
(2, "内部转介绍"),
(3, "官方网站"),
(4, "百度推广"),
(5, "360推广"),
(6, "搜狗推广"),
(7, "腾讯课堂"),
(8, "广点通"),
(9, "高校宣讲"),
(10, "渠道代理"),
(11, "51cto"),
(12, "智汇推"),
(13, "网盟"),
(14, "DSP"),
(15, "SEO"),
(16, "其它"),
]
source = models.SmallIntegerField('客户来源', choices=source_choices, default=1)
referral_from = models.ForeignKey(
'self',
blank=True,
null=True,
verbose_name="转介绍自学员",
help_text="若此客户是转介绍自内部学员,请在此处选择内部学员姓名",
related_name="internal_referral",
on_delete=models.SET_NULL,
)
course = models.ManyToManyField(verbose_name="咨询课程", to="Course")
consultant = models.ForeignKey(verbose_name="课程顾问", to='UserInfo', related_name='consultant',
null=True, blank=True,
limit_choices_to={'depart__title': '销售部'}, on_delete=models.SET_NULL)
education_choices = (
(1, '重点大学'),
(2, '普通本科'),
(3, '独立院校'),
(4, '民办本科'),
(5, '大专'),
(6, '民办专科'),
(7, '高中'),
(8, '其他')
)
education = models.IntegerField(verbose_name='学历', choices=education_choices, blank=True, null=True, )
graduation_school = models.CharField(verbose_name='毕业学校', max_length=64, blank=True, null=True)
major = models.CharField(verbose_name='所学专业', max_length=64, blank=True, null=True)
experience_choices = [
(1, '在校生'),
(2, '应届毕业'),
(3, '半年以内'),
(4, '半年至一年'),
(5, '一年至三年'),
(6, '三年至五年'),
(7, '五年以上'),
]
experience = models.IntegerField(verbose_name='工作经验', blank=True, null=True, choices=experience_choices)
work_status_choices = [
(1, '在职'),
(2, '无业')
]
work_status = models.IntegerField(verbose_name="职业状态", choices=work_status_choices, default=1, blank=True, null=True)
company = models.CharField(verbose_name="目前就职公司", max_length=64, blank=True, null=True)
salary = models.CharField(verbose_name="当前薪资", max_length=64, blank=True, null=True)
date = models.DateField(verbose_name="咨询日期", auto_now_add=True)
last_consult_date = models.DateField(verbose_name="最后跟进日期", auto_now_add=True)
def __str__(self):
return "姓名:{0},联系方式:{1}".format(self.name, self.qq, )
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
客户管理
# -- 客户管理
# http://127.0.0.1:8000/stark/web/customer/public/list/
site.register(models.Customer, PublicCustomerHandler, prev="public") # 公户
# http://127.0.0.1:8000/stark/web/customer/private/list/
site.register(models.Customer, PrivateCustomerHandler, prev="private") # 私户
2
3
4
5
# 公户
(´・Д・)」
# 基本CURD
公户的基本管理 - CURD
1> 列表显示的客户都应该是属于公户的! 所以 列表数据应该通过 钩子get_queryset
提前作一个筛选!
2> 公户的添加和修改, 是不能指定课程顾问的! 所以 应该写个modelform exclude课程顾问这个字段!! (该字段是可为空的,无碍
Q: 思考下, 为啥公户的添加和修改, 是不能指定课程顾问的!?
A: 谁会用公户的添加功能?一般是运营部门的人员. 当有人在官网或qq群里咨询了,就录入该客户的信息.
在录入该客户的信息时,是不能指定课程顾问的. 不然权利太大了,好多销售都会讨好运营人员.
销售应自己申请将某些公户变成自己的私户.或者 由它们的经理来进行分配!! 每个销售的私户个数是由限制的,该系统中我们设定为150个!!
2
3
4
5
# - 跟进记录表
(´・Д・)」
class ConsultRecord(models.Model):
""" 客户跟进记录 """
customer = models.ForeignKey(verbose_name="所咨询客户", to='Customer', on_delete=models.CASCADE)
consultant = models.ForeignKey(verbose_name="跟踪人", to='UserInfo', on_delete=models.CASCADE)
note = models.TextField(verbose_name="跟进内容")
date = models.DateField(verbose_name="跟进日期", auto_now_add=True)
2
3
4
5
6
为了方便测试, 写了个脚本, 往该表里添加了几条记录!
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
for i in range(1, 10):
models.ConsultRecord.objects.create(
customer_id=3,
consultant_id=7,
note=f"第{i}次跟进,(¯﹃¯)!"
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查看跟进记录
查看公户的跟进记录
先来看看效果:
关键代码如下:
在公户里是只能查看跟进记录的,在私户里才可以添加和更改跟进记录.
这里就多了一个url,不是很建议将 跟进记录表的CURD 快速弄出来!
(´・Д・)」 ■ 此处我们自己增加了一个url,自己写相应的视图,自己写前端模版页面!!
2
3
# - 用户登录
用户登录信息会存储到session中, 批量申请到私户时, 从session中拿到当前登录的用户id, 这样才知道申请到哪个销售的私户里啊!!
你要思考一个问题: 是某个用户(销售)登录后, 批量的将多个公户申请到自己的私户中!! 所有我们要先完成用户登录的功能!!
关键代码如下: (我把rbac里的那个登录页面拿了过来Hhhh
# 批量申请到私户
(¯﹃¯)★ 数据库锁的知识
在公户列表上再新增一列, "申请到公户" 一个个的申请吗? No! 我们要实现批量申请!!!
1> 公户里的客户的状态是 已报名的话, 是申请不到自己的 私户的!!
2> 申请到自己私户的客户必须是 公户!! 因为 会IT的可以改checkbox的value值, 这样就可以把别人的私户变成自己的了!
3> 将选中的私户变为当前登陆用户的私户 -> 即将客户表中选中的记录的consultant_id设置为当前登陆用户的id
4> 每个销售的私户个数是有限制的 -> 当然已经报名的私户 是不受限制的!!
5> 事务 + 锁
PS: 在表格中定制了checbox列, 代码截图中没有截到! Hhh
# 私户
(´・Д・)」
# 基本CURD
私户的基本管理
1> 表格中应呈现 当前登陆用户所关联的所有私户数据 -- 重写get_queryset方法
2> 私户的来源有两个:其1是从公户中批量申请;其2是自己给自己添加私户.
给自己添加私户,在添加时,课程顾问是不该填的,应当默认就是自己!! -- exclude+save时指定课程顾问!
为了满足上述需求, 我们对stark组件的save方法的源码进行了一点更改,添加了request
、*args
、*kwarg
参数, 通用性更强啦!!
关键代码如下:
# 批量移除到公户
本质上就是将 客户信息里的 客户顾问字段consultant的值变为空!! 则实现从 私户变为公户! -- 基于action来实现
关键代码如下:
# - 跟进记录的管理
当前登陆用户对自己的私户的跟进记录的管理!!
大体流程如下:
注意事项如下:
针对公户而言,只能对公户的跟进记录进行查看(我们自己写路由写函数);
而对于私户而言,对于私户的跟进记录应实现CURD!! (利用组件自动生成CURD的路由和使用组件自带的视图函数)
# -- 私户里查看跟进记录的四个路由配置!!
path("list/<int:customer_id>/", self.wrapper(self.changelist_view), name=self.get_list_url_name),
path("add/<int:customer_id>/", self.wrapper(self.add_view), name=self.get_add_url_name),
path("edit/<int:customer_id>/<int:pk>/", self.wrapper(self.change_view), name=self.get_change_url_name),
path("del/<int:customer_id>/<int:pk>/", self.wrapper(self.delete_view), name=self.get_delete_url_name),
2
3
4
5
首先, 关于增加按钮、编辑按钮、删除按钮以及跳转回列表页面的相关url的stark组件源码需要修改下.
详看:"※后续:反向生成url的优化"
四个路由为何如此设计呢? 请听我娓娓道来! 请务必结合 ※后续:反向生成url的优化 的内容进行思考..
先得清楚, 所有的跟进记录都在ConsultRecord这一张表里!!
# 查看跟进记录
ψ(`∇´)ψ 私户列表页面 通过该页面的"跟进记录"按钮 与跟进记录页面 进行联动!!
★特别注意:
在私户表格里点击某个私户的 "跟进记录" 按钮后, 展示的应该是该私户的所有跟进记录!!而不是展示所有客户(公户+私户)的跟进记录.
1.So,在私户表格里点击"跟进记录"按钮时,该按钮上a标签的地址应该将该私户的ID传过去,传到哪呢?传到跟进记录的页面!!
★ 跟进记录的列表页面,先重写路由,再重写get_queryset方法,在该方法里拿到传过来的私户ID以及当前登陆用户进行筛选!!
1> 私户表格里 "跟进记录"按钮生成的地址,不用携带原搜索条件!因为加了也用不上,所有不用加.
2> 私户表格里 "跟进记录"按钮生成的地址,_blank 跳转时候新打开一个页面
2. 跟进记录以表格的形式不太好!应类似于前面公户查看跟进记录时的显示时,使用时间轴!
亦或者说 当以后遇到的需求中,若要求表格里显示图片!当前stark组件中通用的列表页面就不好使啦!
解决方案: 我们需要自定义CURD的页面,因此我们需要修改一下stark组件的源码,让其通用性更强!
2
3
4
5
6
7
关键代码如下:
自定义的列表页面 "web/consult_record.html"
, 不自定义的话,默认是表格,当前是 时间轴!
# 跟进记录新增
跟进记录的新增
我们对跟进记录的增、删、改、查的路由进行了以下设计
patterns = [
path("list/<int:customer_id>/", self.wrapper(self.changelist_view), name=self.get_list_url_name),
path("add/<int:customer_id>/", self.wrapper(self.add_view), name=self.get_add_url_name),
path("edit/<int:customer_id>/<int:pk>/", self.wrapper(self.change_view), name=self.get_change_url_name),
path("del/<int:customer_id>/<int:pk>/", self.wrapper(self.delete_view), name=self.get_delete_url_name),
]
2
3
4
5
6
■ 新增 "add/<int:customer_id>/"
customer_id表明给哪个客户添加跟进记录!以及新增完成后跳转回哪个客户的跟进记录列表页面!!
客户跟进记录表ConsultRecord有三个字段: customer、consultant、note、date.
date字段是自动生成的,不会在页面的表单中显示;
customer的值应该是当前这个私户的ID;
consultant的值应该是当前登陆用户的ID.
SO,综上,在新增的表单页面里,只需有note这一个字段!!
特别注意! 在save时, 我们得判断当前这个客户属不属于这个销售/是否是该销售的私户.
防止, 会IT的在url上修改customer_id的参数值后,刷新,点击立即提交按钮后,给其他不属于自己的私户添加跟进记录!!
□ 测试进行验证
- http://127.0.0.1:8000/stark/web/customer/private/list/ 点击某个私户的"跟进记录按钮"
- 跳转 http://127.0.0.1:8000/stark/web/consultrecord/list/3/ 点击新增按钮
- 跳转 http://127.0.0.1:8000/stark/web/consultrecord/add/3/
修改url的参数3,若该值背后所对应的客户不是该登陆用户的私户,那么就跳转到500的提示页面!!
def save(self, request, form, is_update, *args, **kwargs):
if not is_update: # 若是新增
consultant_id = request.session["user_info"]["id"]
customer_id = kwargs.get('customer_id')
object_exists = models.Customer.objects.filter(
id=customer_id,
consultant_id=consultant_id).exists()
if not object_exists:
return render(request, 'stark/500.html', {"msg": "非法操作!我劝你耗自为汁~"})
form.instance.customer_id = customer_id
form.instance.consultant_id = consultant_id
form.save()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 跟进记录修改
跟进记录的修改 get_change_object
我们对跟进记录的增、删、改、查的路由进行了以下设计
patterns = [
path("list/<int:customer_id>/", self.wrapper(self.changelist_view), name=self.get_list_url_name),
path("add/<int:customer_id>/", self.wrapper(self.add_view), name=self.get_add_url_name),
path("edit/<int:customer_id>/<int:pk>/", self.wrapper(self.change_view), name=self.get_change_url_name),
path("del/<int:customer_id>/<int:pk>/", self.wrapper(self.delete_view), name=self.get_delete_url_name),
]
2
3
4
5
6
■ 修改 "edit/<int:customer_id>/<int:pk>/"
pk跟进记录的id,表明对ConsultRecord表的哪条跟进记录进行更新;
customer_id用于修改跟进记录后,能跳转回该客户的跟进记录列表页面.
特别注意! 你仔细回顾修改的过程,因为所有的跟进记录都在ConsultRecord这一张表里!!
所以我们按照默认通常原本的逻辑,只要拿到pk值就可以修改啦!! 这会出现什么bug?
pk值是几,页面显示跟进记录表的哪条跟进记录的内容!!意味着,我可以修改pk值看到不属于自己的私户的跟进记录并进行修改!
So, 在该应用场景下, 我们得保证:
该客户是属于当前登陆用户的私户且更新的该条跟进记录是该客户的!!
解决方案: 详看:"※后续:数据过滤"
□ 测试进行验证
- http://127.0.0.1:8000/stark/web/customer/private/list/ 点击某个私户的"跟进记录按钮"
- 跳转 http://127.0.0.1:8000/stark/web/consultrecord/list/3/ 点击该私户的某条跟进记录的"修改按钮"
- 跳转 http://127.0.0.1:8000/stark/web/consultrecord/edit/3/18/
此时我们看到的是id值为3的这个客户在ConsultRecord跟进记录表中所关联的id值为18的跟进记录的内容!!
若此时,我将18的值改成10 或者 我将3改成1, 因为该跟进记录不是该客户的,所以会跳转500的提示页面!!
def get_change_object(self, request, pk, *args, **kwargs):
customer_id = kwargs.get('customer_id')
current_user_id = request.session['user_info']['id']
return models.ConsultRecord.objects.filter(
pk=pk,
customer_id=customer_id,
customer__consultant_id=current_user_id).first()
# - pk是否存在 customer__consultant_id该客户是否是当前登陆用户的私户 customer_id该跟进记录是否属于客户
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 跟进记录删除
跟进记录的删除 get_delete_obj
跟跟进记录的修改同理, 不再赘述!!
特别注意: 若可以改pk值 将不属于它的跟进记录删除掉,这是不允许的..
So, 在该应用场景下, 我们得保证:
该客户是属于当前登陆用户的私户且更新的该条跟进记录是该客户的!!
□ 测试进行验证 跟跟进记录的修改的测试验证类似!!
def get_delete_obj(self, request, pk, *args, **kwargs):
customer_id = kwargs.get('customer_id')
current_user_id = request.session['user_info']['id']
return models.ConsultRecord.objects.filter(
pk=pk,
customer_id=customer_id,
customer__consultant_id=current_user_id).first()
# - pk是否存在 customer__consultant_id该客户是否是当前登陆用户的私户 customer_id该跟进记录是否属于客户
2
3
4
5
6
7
8
9
# 关键代码
关键代码如下:
懒得搞了,直接粘贴代码, 撇脱(四川话) Hhh!
# 缴费和报名申请
客户缴费 - 该客户对应的课程顾问提交缴费申请 - 财务进行审核 - 审核成功后,状态更新+入班学习
其实缴费记录的管理和跟进记录的管理, 实现的代码逻辑都差不多的!! 可以彼此借鉴思考.
# 数据表的设计
缴费申请表和学生表
设计的表的代码如下:
class PaymentRecord(models.Model):
""" 缴费申请 """
customer = models.ForeignKey(Customer, verbose_name="客户", on_delete=models.CASCADE)
consultant = models.ForeignKey(
verbose_name="课程顾问", to='UserInfo', help_text="谁签的单就选谁", on_delete=models.CASCADE)
pay_type_choices = [
(1, "报名费"),
(2, "学费"),
(3, "退学"),
(4, "其他"),
]
pay_type = models.IntegerField(verbose_name="费用类型", choices=pay_type_choices, default=1)
paid_fee = models.IntegerField(verbose_name="金额", default=0)
# ■ 选择的费用类型是"其他"的话,class_list的值是可以为空的,这里没有设置可为空,是简化处理了!
class_list = models.ForeignKey(verbose_name="申请班级", to="ClassList", on_delete=models.CASCADE)
apply_date = models.DateTimeField(verbose_name="申请日期", auto_now_add=True)
# ■ 财务审核后,以下四个字段才有值
confirm_status_choices = (
(1, '申请中'),
(2, '已确认'),
(3, '已驳回'),
)
confirm_status = models.IntegerField(verbose_name="确认状态", choices=confirm_status_choices, default=1)
confirm_date = models.DateTimeField(verbose_name="确认日期", null=True, blank=True)
confirm_user = models.ForeignKey(
verbose_name="审批人", to='UserInfo',
related_name='confirms', null=True, blank=True,on_delete=models.SET_NULL)
note = models.TextField(verbose_name="备注", blank=True, null=True)
class Student(models.Model):
""" 学生表 """
customer = models.OneToOneField(verbose_name='客户信息', to='Customer', on_delete=models.CASCADE)
# ■ 客户表里那里的信息可能是乱填的;而在学生表这,qq、手机号这些得是真实的!
qq = models.CharField(verbose_name='QQ号', max_length=32)
mobile = models.CharField(verbose_name='手机号', max_length=32)
emergency_contract = models.CharField(verbose_name='紧急联系人电话', max_length=32)
# ■ n:n 一个学员可以报很多个班级
class_list = models.ManyToManyField(verbose_name="已报班级", to='ClassList', blank=True)
# ■ 在新增缴费记录时添加的学生信息,还没有审批通过,所以状态是在申请中!!
student_status_choices = [
(1, "申请中"),
(2, "在读"),
(3, "毕业"),
(4, "退学")
]
student_status = models.IntegerField(verbose_name="学员状态", choices=student_status_choices, default=1)
score = models.IntegerField(verbose_name='积分', default=100)
memo = models.TextField(verbose_name='备注', max_length=255, blank=True, null=True)
def __str__(self):
return self.customer.name
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 缴费记录的查看
(¯﹃¯)
需求分析:
1> 在私户列表里,添加一列 缴费记录, 点击它, 会将该行的私户的缴费记录呈列出来!(类似于跟进记录的点击
-- a标签反向生成联动过去 将客户ID带过去!
2> 该私户得是当前登陆用户的,呈列的是该私户的缴费记录 -- 重写get_queryset
3> 缴费记录只能查看和添加,不能编辑和删除!因为它不是儿戏.. 即表格中没有编辑和删除按钮! -- 重写get_list_display、get_urls
2
3
4
关键代码如下:
# 缴费记录的新增
(´・Д・)」
需求分析
- 客户字段是不能选的,点击时已经传递过来了
- 课程顾问ID也不能选,是当前登陆的用户
- 确认状态、确认日期、审批人 都不应该显示.
- 确认状态一开始就是是申请中,财务审核后 状态才变更为 已确认或已驳回
- 确认日期是审核的时候填
- 审批人应该是审批的那个人
So,自定义ModelForm来控制新增页面显示的字段 + save保存时,修改instance
- 还需思考一点,新增时,是否需要添加该缴费客户真实的信息
1. 若是第一次缴费 需要;否则不需要,用原来第一次缴费时登机的学员信息即可
即以前是否报过名!
解决方案:
两个modelform,一个有学员信息(自定义字段!!qq、mobile、emergency_contract)、一个没有学员信息
然后通过预留的钩子函数get_model_form_class中判定使用哪一个! -- 源码中改进下该钩子函数的参数
无需担心,缴费记录表相关的modelform在save时,与数据库无关的字段qq、mobile、emergency_contract会自动剔除!
2. 那么学生表呢?
先要判断哦!有该学员信息就不管,没有的话:
就去form对象中拿到字段qq、mobile、emergency_contract的信息手动创建学生表的一条记录!
那已报班级咋整了!当然可不管,要管的话,就是多对多的添加
- 手动创建拿到student_object后,student_object.class_list.add[form.cleaned_data["class_list"].id]
Ps: class_list = form.cleaned_data['class_list'] print(class_list,type(class_list))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
关键代码如下:
# 缴费的审批
╮( ̄▽ ̄"")╭
需求分析
缴费状态的审批 - 缴费记录状态更新、学员状态更新、客户状态的更新,修改三张表!
= 财务看到的是所有的缴费记录 现有缴费记录表相关的handler是客户相关的;现在我们创建一个跟财务审核缴费记录相关的,记得加前缀!
- 列表显示的客户一列,最好跨表过去展示客户关联的学生表里的真实信息
- 财务应该只能审核,添加、编辑、删除的功能都应该没有!
- get_list_display、get_add_btn 不显示了
- get_urls重写路由
- 不建议每一行都添加一个审核按钮,建议批量审核 --> 三个批量操作:批量通过、批量驳回、批量退学
注意:处于申请中的状态才能进行驳回! 即思考:已通过后再次通过、已通过后再驳回
- 列表排序,从大到小+状态
2
3
4
5
6
7
8
9
关键代码如下:
site.register(models.PaymentRecord, PaymentRecordHandler) # 私户的缴费记录的查看和添加
site.register(models.PaymentRecord, CheckPaymentRecordHandler, 'check') # 财务对缴费的审批
2