一、开发背景
由于公司人员的增多,原来通过excel表格存取方式过于繁琐,而且对于公司人员的调配和绩效考核等不能做到精确处理,所以开发crm系统,开始开发只是针对销售人员和客户,后面陆续加上一些操作,如学校管理和教师管理,课程管理等,
二、开发周期
开发2个月,2个月后持续还在做:修复bug和新功能的开发。
最初只是开发了业务,因为时间比较紧,后面由于维护和和更有利于新功能的扩展,重新抽取组件,如stark组件和分页组件,rbac(权限组件)
三、表设计
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
四、 主要业务
销售业务
销售业务说明(customer,SaleRank权重表 CustomerDistribution客户分配表)
一、目的
主要针对的是公司销售部门的工作管理,主要用于工作安排、销售进度的跟进以及为业绩考核提供数据支持。
二、业务机制
1.工作安排原则(分配订单原则)
根据对销售人员的以往业绩的分析制定每个销售人员的销售任务,综合多方面数据对销售人员进行权重的评定。在工作分配时,
通过权重进行排序,权重大的优先安排任务。分配任务时,按照权重排序从高到低顺位循环分配。(
若销售人员被分配任务以达到目标值则跳过,若所有销售人员都达到目标值则将剩余销售任务重复之前的方案进行分配)。
实现原理:
创建权重表,包含字段:user,权重,转化人数,创建一个类,获取权重表里面的所有销售人员并根据权重进行降序排序,根据权重和转换人数
将销售的id依次循环放入列表中,直到等于销售人员的转化人数就跳过该销售人员,在后台调用即可,当循环完以后,调用
reset()重置即可,主要使用Redis来完成(原因:减轻内存压力,普通放到内存中关机或断电就没了,这样不公平,Redis可以处理这个麻烦,其次Redis可以有效的处理多进程问题)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
2.销售进度状态
销售人员得到任务后对应的客户状态改为开始接洽,记录起始时间,订单状态从公司资源更改为销售人员的个人资源,其他人在订单转移前不可接触订单信息。
销售人员在跟进订单时,每一次与客户接洽都会在数据库中生成一条记录。
若订单在十五日内被销售人员转化成功,则将该客户的状态由待转化变为转化成功,并在正式客户表中生成该客户的记录。在销售人员的订单记录中将这笔订单的状态改为转化成功。
若当前与客户接洽的销售人员三天未跟进订单或是在十五天内未促成交易。则相关订单信息会被移动到公司公共资源中,并且原先跟进订单的销售人员不可以选择继续跟进(直至该订单再次被移入公司公共资源)。原销售人员的订单跟进记录中会显示有一单未能转化,并显示原因(重新接手该订单后即使转化成功,本条记录不会被覆盖)。
实现原理:
a.客户来源:
1.来源一:运营部所分配的,权重分配一样(具体操作一样见上面)
2.来源二:抢单,从公司公共资源池里面获取:条件这个人不能是自己转化过de,或者是其他销售接单的但是接单超过15天
或超过3天没有联系的,点击抢单进行获取,将该用户添加到我的客户中(在客户分配表中创建一条新的记录),并且status默认
为正在根进,将该客户在客户表中的跟进人换成当前登录用户,接单时间和最近跟进时间换成当前时间即可
3.来源三:自己录入的
b.未转换成功(订单失效):当接单超过15天或者连续三天没有联系字该客户自动进入公共资源池,定时器完成
c.转换成功:转换成功后 加入我的客户中(在客户分配表中创建一条新的记录),并且status默认
为已转换
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
3.销售数据
销售人员所接触过的每一个客户,不管什么来源,转换成功以否都会保存起来,为以后的权重划分和绩效考核为依据,
实现原理:
通过实现查看我的客户就可以一目了然的看到该销售人员的所有客户
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
4.业绩考核
在销售人员的订单记录中记录了销售人员从第一笔业务到最近一笔业务的所有信息。可以为销售人员的业绩考核提供:接单数,转化率,订单未转化原因等数据。
学校管理业务说明 (courserecord,studyrecord)
一、系统用途
主要服务于教育机构,对教学班级,校区,课程,学生等进行管理,主要用于班级的成员管理、课堂出勤情况记录以及学员成绩记录。
二、业务机制
1.成员管理
销售人员与客户接洽完毕,将客户转化为学员后,根据其选择的校区、课程、班级将其信息录入学员的数据库中。初始化该学员的账号
和密码,以便其进入教学管理系统查看自己的成绩以及出勤记录。若该学员因某些原因中途退学或进入其他班级,则将其记录删除
(出勤记录与成绩记录详见2、3条说明)。
2.课堂出勤情况记录
每日上课前由班主任或当日讲师初始化当日的考勤信息,初始化时默认全部全员正常出勤。如有学员存在:迟到、旷到、早退或请假等情况。可由班主任或当日讲师修改其考勤状况(支持批量修改)。
若有学生中途进入班级,进班前的考勤记录不与不予生成。若有学生中途离开教学班级,离班前的考勤记录不予删除。
上课教师和班主任对学生进行考勤管理,考勤直接影响这节课的成绩,考勤种类为已签到,迟到,缺勤,早退,请假.
初始化实现原理:
点击复选框选中要初始化的当天班级课程,点击action中学生初始化对学生完成初始化 默认全部出勤,
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
如果有个别学生出现违规情况,在studyrecord中对该学生进行操作
实现原理:
教师和班主任在课程记录页面点击考勤管理,调转到该班级的学习记录页面,列出该班级的所有学生,利用action对学生进行批量的
考勤管理
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
3.学员成绩记录
在班主任或当日讲师进行初始化考勤信息操作时可以选择当日是否有作业(支持修改)。若当日有作业则开放学生作业提交的功能,
学生须在提交时间内提交,提交时间结束关闭该功能。
学生提交作业后在提交时间内允许撤销提交并重新提交。提交时间结束后,助教可以下载学员提交文件,并进行打分评定。
打分和评定评语结束后可立即上传至教学管理系统,学生可以通过教学管理系统进行查询(支持批量导入)。
若有学生中途进入班级,进班前的成绩记录不与不予生成。若有学生中途离开教学班级,离班前的成绩记录不予删除。
成绩录入实现原理:
在课程记录页面点击成绩录入调到成绩录入页面,主要是根据当前趁机记录的id获取该节课所有的学生记录对象,发送dao前端,
其中由于成绩打分要严格区分,所欲成绩和评语有type动态生成ModelForm对象传到前端,fields字段分别是score_学生记录id
,homework_note_学生记录对象id,post传到后端 的时候在courseRecordConfig中看代码成绩录入
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
会议室预定 业务
公司人员增多,空间有限,会议室需要被预定才可使用,每个会议室从早上八点-晚上八点可以预定,一小时划分,如果该会议室当前时间被预定了,如果预定的人是自己,再点击则取消,如果是别人预定的则不可以点击
,如果没有没预定点击则预定,
调查问卷 业务
调查问卷是为了调查学生对学校设备和讲师讲课的满意程度,以及他没有什么困难等,获取他们的意见以方便我们进行改进,问卷只能有班主任和教务总监发起,并明确的规定班级和起始日期和结束时间,并且只有本班学生才能填写,调查问卷的题型有三种,填写内容(建议),单选,打分。学生打分后点击提交即可完成调查问卷,学生提交后,在首页可显示答卷的人数。
技术点:
1. 通过ChangeList封装好多数据
原因:
change_listview列表页面代码太多啦 而却有好几个功能,代码结构不清晰,修改代码麻烦,传到前端的东西过多,封装后代码结构清晰,解耦性提高,便于维护,而且在changelist_view中只需要传入changelist对象即可。
2. 销售中公共资源:Q查询,3天 15天
销售接单后开始记录时间,如果三天 未跟进十五天未成单,该客户进入公司的公共资源池,并且当前销售人员不能在公共资源池里面对该客户没有任何权限.
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
3. 使用yield实现(前端需要循环生成数据时)
- 生成器函数,对数据进行加工处理
- __iter__和yield配合
组合搜索时用到先在ChangList中的get_combine_seach_filter()中返回row对象,然后在FilterRow类中创建__iter__方法 yied生成每个组合搜索所对应的url
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
4. 获取Model类中的字段对应的对象
class Foo(model.Model):
xx = models.CharField()
Foo.get_field('xx')
根据类名获取相关字段
model.UserInfo model.UserInfo._meta.app_label#获取当前app的名称 model.UserInfo._meta.model_name#获取当前类名小写 model.UserInfo._meta.get_field('username')#获取字段 model.UserInfo._meta.get_field('username').verbose_name#获取verbose_name model.UserInfo._meta.get_field('外键或多对多字段').rel.to #得到关联的model类 - models.UserInfo._meta.get_field('name') # 根据字段名称,获取字段对象 - models.UserInfo._meta.fields # 获取类中所有的字段 - models.UserInfo._meta._get_fields() # 获取类中所有的字段(包含反向关联的字段) - models.UserInfo._meta.many_to_many # 获取m2m字段
获取当前类的对象所反向关联的字段
obj所得到的对象 related_fileds=obj._meta.related_objects #得到当前对象的反向关联的所有字段 for related_field in fileds: _model_name=related_field.field.model._meta.model_name#获取当前关联字段所属的类名 _related_name=related_field.related_name#获取当前字段中的_related_name(反向查找的别名) _field_name=related_field.field_name#获取当前字段跟其他表所关联的字段(to_field='') _limit_choices_to=related_obj.limit_choices_to
5. 模糊搜索功能
用到Q查询
根据show_search_form判断是否显示模糊搜索框,search_fileds=[]代表可以以什么搜索
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
6. Type创建类
主要用于动态生成modelForm时用到,在调查问卷和成绩录入是用到
Type中第一个参数是类名,第二个是继承的类,第三个是字典,其中我们操作主要是在字典中进行操作
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
7. 自动派单
- 原来在内存中实现,问题:重启和多进程时,都有问题。
- redis
- 状态
- 原来数据(权重表 权重和个数)
- pop数据
上面 业务中有具体体说明
8. 使用 list_diplay配置
list_display = [函数名,字段名。。。。]
9. reverse反向生成URL
根据url中name字段的值利用reverse生成,如果有namespace则需要在最前面加上,并用“:””分隔,url中有参数还需要传参args=[]
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
10. 母版
模板的继承
模板中 {%block body%}{%endblock%}
子版中最顶行{% extends '母版的路径' %}
{%block body%}{%endblock%}
11. ready方法定制起始文件
- 文件导入实现单例模式
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
12. inclusion_tag
在权限管理生成菜单和crm中生成popup数据时用到
当前所装饰的函数所得到的值,传到inclusion_tag中的html中使用,(这个html一般是一个子版),如果有模板需要用到这个html模板,则需要在当前模板中
{% inclusion_tag所修饰的函数名 参数一 参数二....%}
13. 中间件的使用
登录和权限管理用到,
需要继承MiddlewareMixin,有五个方法:
- process_request(self,request)
- process_response(self, request, response
- process_view(self, request, callback, callback_args, callback_kwargs)
- process_template_response(self,request,response)
- process_exception(self, request, exception)
15. importlib + getattr
在发送消息是用到,参考django源码可知,中间件也是采用这种方法
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
import importlib for cls_path in settings.MESSAGE_CLASSES: # cls_path是字符串 module_path,class_name = cls_path.rsplit('.',maxsplit=1) m = importlib.import_module(module_path) obj = getattr(m,class_name)() obj.send(subject,body,to,name)
16. FilterOption,lambda表达式
目的是为了判断关联表的关联字段是不是主键还是其他字段
17. QueryDict
- 原条件的保留
- filter
http://www.cnblogs.com/ctztake/p/8076003.html
18. ModelForm
可以自定义也可以使用satrkcofig中的
type生成ModelForm
这里重点说form的循环
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
19. 面向对象的 @property @classmethod
20. mark_safe
在后台写的html传到前端能够正常显示,目的是为了防止xss攻击
还有一种类似的方法,直接在前端 {{aaa|safe}}
21. 抽象方法抽象类+raise Im...
在发送消息时用到,要求所有继承BaseMessage的类都必须实现send()方法,没有继承则抛出异常
class BaseMessage(object): def send(self, subject, body, to, name): raise NotImplementedError('未实现send方法')
当然也可以用接口实现,但相对于上面那种方法过于繁琐,推荐使用上面的方法
22. 组件中的装饰器,实现self.request = request
23. js自执行函数
(function(arg){
})('sf')
24. URL的钩子函数
25. 多继承
python3中都是新式类,遵从广度优先
python2中既有经典类和新式类,经典类是指当前类和父类都没有继承obj类,新式类是指当前类或其父类只要有继承了obj类就算新式类
经典类遵循深度优先
新式类遵循广度优先
26. 批量导入,xlrd
27. redis连接池
28. 工厂模式
工厂模式实际上包含了3中设计模式,简单工厂,工厂和抽象工厂,关键点如下: 一、三种工厂的实现是越来越复杂的 二、简单工厂通过构造时传入的标识来生产产品,不同产品都在同一个工厂中生产,这种判断会随着产品的增加而增加,给扩展和维护带来麻烦 三、工厂模式无法解决产品族和产品等级结构的问题 四、抽象工厂模式中,一个工厂生产多个产品,它们是一个产品族,不同的产品族的产品派生于不同的抽象产品(或产品接口)。 好了,如果你能理解上面的关键点,说明你对工厂模式已经理解的很好了,基本上面试官问你工厂模式,你可以昂头挺胸的说一番。但是,面试官怎么可能会放过每一次虐人的机会?你仍然可能面临下面的问题: 1. 在上面的代码中,都使用了接口来表达抽象工厂或者抽象产品,那么可以用抽象类吗?有何区别? 从功能上说,完全可以,甚至可以用接口来定义行为,用抽象类来抽象属性。抽象类更加偏向于属性的抽象,而用接口更加偏向行为的规范与统一。使用接口有更好的可扩展性和可维护性,更加灵活实现松散耦合,所以编程原则中有一条是针对接口编程而不是针对类编程。 2. 到底何时应该用工厂模式 根据具体业务需求。不要认为简单工厂是用switch case就觉得一无是处,也不要觉得抽象工厂比较高大上就到处套。我们使用设计模式是为了解决问题而不是炫技,所以根据三种工厂模式的特质,以及对未来扩展的预期,来确定使用哪种工厂模式。 3.说说你在项目中工厂模式的应用 crm项目中发送消息是用到,因为我们要同时发短信,微信,钉钉,和邮件信息,我们把他包装在一个baseMessage中,使用时直接调用baseMessage的send()即可
settings.py
MSG_PATH = "path.Email"
class XXFactory(object):
@classmethod
def get_obj(cls):
settings.MSG_PATH
# rsplit
# importlib
# getattr
return obj
class Email(object):
def send ...
class WeChat(object):
def send ...
class Msg(object):
def send ...
29. Models类中自定义save方法
30. django admin中注册models时候
from django.contrib import admin
from . import models
# 方式一
class UserConfig(admin.ModelAdmin):
pass
admin.site.register(models.UserInfo,UserConfig)
# 方式二
@admin.register(models.UserInfo)
class UserConfig(admin.ModelAdmin):
pass
31. 深浅拷贝
1.对于数字
和字符串
而言,赋值、浅拷贝和深拷贝无意义,因为他们的值永远都会指向同一个内存地址。
2.对于字典、元祖、列表 而言,进行赋值、浅拷贝和深拷贝时,其内存地址的变化是不同的。
赋值,只是创建一个变量,该变量指向原来内存地址
浅拷贝,在内存中只额外创建第一层数据
# 导入拷贝模块 >>> import copy >>> var1 = {"k1": "1", "k2": 2, "k3": ["abc", 456]} # 使用浅拷贝的方式 >>> var2 = copy.copy(var1) # 两个变量的内存地址是不一样的 >>> id(var1) 2084726354952 >>> id(var2) 2084730248008 # 但是他们的元素内存地址是一样的 >>> id(var1["k1"]) 2084726207464 >>> id(var2["k1"]) 2084726207464
深拷贝,在内存中将所有的数据重新创建一份(排除最后一层,即:python内部对字符串和数字的优化)
# 导入拷贝模块 >>> import copy >>> var1 = {"k1": "1", "k2": 2, "k3": ["abc", 456]} # 使用深拷贝的方式把var1的内容拷贝给var2 >>> var2 = copy.deepcopy(var1) # var1和var2的内存地址是不相同的 >>> id(var1) 1706383946760 >>> id(var2) 1706389852744 # var1和var2的元素"k3"内存地址是不相同的 >>> id(var1["k3"]) 1706389853576 >>> id(var2["k3"]) 1706389740744 # var1和var2的"k3"元素的内存地址是相同的 >>> id(var1["k3"][1]) 1706383265744 >>> id(var2["k3"][1]) 1706383265744
印象深刻的东西
- 组合搜索时,生成URL: - request.GET - 深拷贝 - 可迭代对象 - yield - 面向对象封装 - popup - window.open("",'name') - opener.xxxxxxx() - FK时,可以使用 limit_choice_to 可以是字典和Q对象 - related_name和model_name - 获取所有的反向关联字段,获取limit_choice_to字段 - 查询 - excel批量导入 - 路由系统 - 动态生成URL - 看Admin源码(include) - /xx/ -> ([ 'xxx', ],namespace) - 开发组件时,最开始看到admin源码不太理解,当和权限系统配合时,才领悟其中真谛。开始想的只要用add_btn=True,show_searche=True等等就可以了,为什么还要用get_add_btn()
和get_show_search等等,后来开发组件进行权限管理时才明白,这都是预留给权限用的,根据继承的先后顺序和登录用户所拥有的权限判断是否显示按钮等。
-学生录入成绩时,为了区分是给那个学生录成绩,并且在后台获取的时候能够区分这个成绩和评语是给那个学生的 利用了type动态生成form还有 动态生成field字段