crm项目整理
一、开发背景
由于公司人员的增多,原来通过excel表格存取方式过于繁琐,而且对于公司人员的调配和绩效考核等不能做到精确处理,所以开发crm系统,开始开发只是针对销售人员和客户,后面陆续加上一些操作,如学校管理和教师管理,课程管理等,
二、开发周期
开发2个月,2个月后持续还在做:修复bug和新功能的开发。
最初只是开发了业务,因为时间比较紧,后面由于维护和和更有利于新功能的扩展,重新抽取组件,如stark组件和分页组件,rbac(权限组件)
三、表设计
from django.db import models from rbac import models as rbac_model class Department(models.Model): """ 部门表 市场部 1000 销售 1001 """ title = models.CharField(verbose_name='部门名称', max_length=16) code = models.IntegerField(verbose_name='部门编号',unique=True,null=False) def __str__(self): return self.title class UserInfo(models.Model): """ 员工表 """ auth = models.OneToOneField(verbose_name='用户权限', to=rbac_model.User,blank=True,null=True) name = models.CharField(verbose_name='员工姓名', max_length=16) username = models.CharField(verbose_name='用户名', max_length=32) password = models.CharField(verbose_name='密码', max_length=64) email = models.EmailField(verbose_name='邮箱', max_length=64) depart = models.ForeignKey(verbose_name='部门', to="Department",to_field="code") def __str__(self): return self.name class Course(models.Model): """ 课程表 如: Linux基础 Linux架构师 Python自动化开发精英班 Python自动化开发架构师班 """ name = models.CharField(verbose_name='课程名称', max_length=32) def __str__(self): return self.name class School(models.Model): """ 校区表 如: 北京海淀校区 北京昌平校区 上海虹口校区 广州白云山校区 """ title = models.CharField(verbose_name='校区名称', max_length=32) def __str__(self): return self.title class ClassList(models.Model): """ 班级表 如: Python全栈 面授班 5期 10000 2017-11-11 2018-5-11 """ school = models.ForeignKey(verbose_name='校区', to='School') course = models.ForeignKey(verbose_name='课程名称', to='Course') semester = models.IntegerField(verbose_name="班级(期)") price = models.IntegerField(verbose_name="学费") start_date = models.DateField(verbose_name="开班日期") graduate_date = models.DateField(verbose_name="结业日期", null=True, blank=True) memo = models.CharField(verbose_name='说明', max_length=256, blank=True, null=True, ) teachers = models.ManyToManyField(verbose_name='任课老师', to='UserInfo', related_name='teach_classes',limit_choices_to={'depart_id__in':[1003,1004,]}) tutor = models.ForeignKey(verbose_name='班主任', to='UserInfo', related_name='classes',limit_choices_to={"depart_id":1002}) def __str__(self): return "{0}({1}期)".format(self.course.name, self.semester) class Customer(models.Model): """ 客户表 """ qq = models.CharField(verbose_name='qq', max_length=64, unique=True, help_text='QQ号必须唯一') name = models.CharField(verbose_name='学生姓名', max_length=16) gender_choices = ((1, '男'), (2, '女')) gender = models.SmallIntegerField(verbose_name='性别', choices=gender_choices) 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) 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" ) course = models.ManyToManyField(verbose_name="咨询课程", to="Course") status_choices = [ (1, "已报名"), (2, "未报名") ] status = models.IntegerField( verbose_name="状态", choices=status_choices, default=2, help_text=u"选择客户此时的状态" ) consultant = models.ForeignKey(verbose_name="课程顾问", to='UserInfo', related_name='consultant',limit_choices_to={'depart_id':1001}) date = models.DateField(verbose_name="咨询日期", auto_now_add=True) last_consult_date = models.DateField(verbose_name="最后跟进日期", auto_now_add=True) recv_date = models.DateField(verbose_name='接单时间', null=True, blank=True) def __str__(self): return "姓名:{0},QQ:{1}".format(self.name, self.qq, ) class CustomerDistribution(models.Model): """ 客户分配表 """ user = models.ForeignKey(verbose_name='客户顾问',to='UserInfo',related_name='cds',limit_choices_to={'depart_id':1000}) customer = models.ForeignKey(verbose_name='客户',to='Customer',related_name='dealers') ctime = models.DateField() status_choices = ( (1,'正在跟进'), (2,'已成单'), (3,'3天未跟进'), (4,'15天未成单'), ) status = models.IntegerField(verbose_name='状态',choices=status_choices,default=1) memo = models.CharField(verbose_name='更多信息',max_length=255) class SaleRank(models.Model): """ 销售权重和数量 """ user = models.ForeignKey(to="UserInfo",limit_choices_to={'depart_id':1000}) num = models.IntegerField(verbose_name='数量') weight = models.IntegerField(verbose_name='权重') class ConsultRecord(models.Model): """ 客户跟进记录 """ customer = models.ForeignKey(verbose_name="所咨询客户", to='Customer') consultant = models.ForeignKey(verbose_name="跟踪人", to='UserInfo') date = models.DateField(verbose_name="跟进日期", auto_now_add=True) note = models.TextField(verbose_name="跟进内容...") class PaymentRecord(models.Model): """ 缴费记录 """ customer = models.ForeignKey(Customer, verbose_name="客户") class_list = models.ForeignKey(verbose_name="班级", to="ClassList", blank=True, null=True) pay_type_choices = [ (1, "订金/报名费"), (2, "学费"), (3, "转班"), (4, "退学"), (5, "退款"), ] pay_type = models.IntegerField(verbose_name="费用类型", choices=pay_type_choices, default=1) paid_fee = models.IntegerField(verbose_name="费用数额", default=0) turnover = models.IntegerField(verbose_name="成交金额", blank=True, null=True) quote = models.IntegerField(verbose_name="报价金额", blank=True, null=True) note = models.TextField(verbose_name="备注", blank=True, null=True) date = models.DateTimeField(verbose_name="交款日期", auto_now_add=True) consultant = models.ForeignKey(verbose_name="负责老师", to='UserInfo', help_text="谁签的单就选谁") class Student(models.Model): """ 学生表(已报名) """ customer = models.OneToOneField(verbose_name='客户信息', to='Customer') username = models.CharField(verbose_name='用户名', max_length=32) password = models.CharField(verbose_name='密码', max_length=64) emergency_contract = models.CharField(max_length=32, blank=True, null=True, verbose_name='紧急联系人') class_list = models.ManyToManyField(verbose_name="已报班级", to='ClassList', blank=True) company = models.CharField(verbose_name='公司', max_length=128, 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) date = models.DateField(verbose_name='入职时间', help_text='格式yyyy-mm-dd', blank=True, null=True) memo = models.CharField(verbose_name='备注', max_length=256, blank=True, null=True) def __str__(self): return self.username class CourseRecord(models.Model): """ 上课记录表 """ class_obj = models.ForeignKey(verbose_name="班级", to="ClassList") day_num = models.IntegerField(verbose_name="节次", help_text=u"此处填写第几节课或第几天课程...,必须为数字") teacher = models.ForeignKey(verbose_name="讲师", to='UserInfo') date = models.DateField(verbose_name="上课日期", auto_now_add=True) 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=True, verbose_name="本节有作业") 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 StudyRecord(models.Model): course_record = models.ForeignKey(verbose_name="第几天课程", to="CourseRecord") student = models.ForeignKey(verbose_name="学员", to='Student') record_choices = (('checked', "已签到"), ('vacate', "请假"), ('late', "迟到"), ('noshow', "缺勤"), ('leave_early', "早退"), ) record = models.CharField("上课纪录", choices=record_choices, default="checked", max_length=64) 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'), ) score = models.IntegerField("本节成绩", choices=score_choices, default=-1) 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) 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) def __str__(self): return "{0}-{1}".format(self.course_record, self.student)
from django.db import models class Menu(models.Model): ''' 菜单表 ''' title=models.CharField(max_length=32,verbose_name="菜单名称") def __str__(self): return self.title class Group(models.Model): ''' 权限组 ''' caption=models.CharField(max_length=32,verbose_name="组名称") menu=models.ForeignKey(verbose_name="所属菜单",to="Menu",default=1) def __str__(self): return self.caption class Permission(models.Model): """ 权限表 """ title = models.CharField(verbose_name='标题',max_length=32) url = models.CharField(verbose_name="含正则URL",max_length=64) # is_menu = models.BooleanField(verbose_name="是否是菜单") menu_gp=models.ForeignKey(verbose_name="组内菜单",to="Permission",null=True,blank=True) code=models.CharField(max_length=32,verbose_name="代码",default="list") group=models.ForeignKey(verbose_name="s所在权限组",to="Group",default=1) class Meta: verbose_name_plural = "权限表" def __str__(self): return self.title class User(models.Model): """ 用户表 """ username = models.CharField(verbose_name='用户名',max_length=32) password = models.CharField(verbose_name='密码',max_length=64) email = models.CharField(verbose_name='邮箱',max_length=32) roles = models.ManyToManyField(verbose_name='具有的所有角色',to="Role",blank=True) class Meta: verbose_name_plural = "用户表" def __str__(self): return self.username class Role(models.Model): """ 角色表 """ title = models.CharField(max_length=32) permissions = models.ManyToManyField(verbose_name='具有的所有权限',to='Permission',blank=True) class Meta: verbose_name_plural = "角色表" def __str__(self): return self.title
from django.db import models # Create your models here. class UserInfo(models.Model): ''' 用户表 ''' username=models.CharField(max_length=32,verbose_name="用户名") pwd=models.CharField(max_length=32,verbose_name="密码") def __str__(self): return self.username class Meta: verbose_name_plural="用户表" class ClassList(models.Model): ''' 班级表 ''' title=models.CharField(max_length=64,verbose_name="班级名称") def __str__(self): return self.title class Meta: verbose_name_plural="班级表" class Student(models.Model): ''' 学生表 ''' username = models.CharField(max_length=32,verbose_name="姓名") pwd=models.CharField(max_length=32,verbose_name="姓名") cls = models.ForeignKey(to=ClassList,verbose_name="所属班级") def __str__(self): return self.username class Meta: verbose_name_plural="学生表" class QuestionSuv(models.Model): ''' 问卷表 ''' title=models.CharField(max_length=32,verbose_name="问卷名称") create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True) end_time = models.DateTimeField(verbose_name="结束时间",null=True) classobj=models.ForeignKey(to=ClassList,verbose_name="班级") userInfoobj=models.ForeignKey(to=UserInfo,verbose_name="创建人") def __str__(self): return self.title class Meta: verbose_name_plural="问卷表" class Question(models.Model): ''' 问题表 ''' title=models.CharField(max_length=32,verbose_name="问题名称") question_type=( (1,"单选"), (2,"打分"), (3,"建议") ) type=models.IntegerField(choices=question_type,verbose_name="问题类型") questionSuv=models.ManyToManyField(to=QuestionSuv,verbose_name="所属问卷") def __str__(self): return self.title class Meta: verbose_name_plural="问题表" class Option(models.Model): ''' 单选表 ''' title=models.CharField(max_length=32,verbose_name="选项名称") value=models.IntegerField(verbose_name="选项所对应的分值") question = models.ForeignKey(to=Question,verbose_name="所属问题") def __str__(self): return self.title class Meta: verbose_name_plural="单选表" class Answer(models.Model): ''' 答案表 ''' stu=models.ForeignKey(to=Student,verbose_name="所属学生") question=models.ForeignKey(to=Question,verbose_name="所属问题") option=models.ForeignKey(to=Option,null=True,blank=True,verbose_name="所属单选表") value=models.IntegerField(null=True,blank=True,verbose_name="打分表的分值") content=models.CharField(max_length=255,null=True,blank=True,verbose_name="建议表的内容") questionSuv=models.ForeignKey(to=QuestionSuv,null=True) class Meta: verbose_name_plural="答案表"
from django.db import models # Create your models here. class Room(models.Model): name=models.CharField(max_length=32,verbose_name='会议室名称') def __str__(self): return self.name class Meta: verbose_name_plural='会议室' class UserInfo(models.Model): name=models.CharField(max_length=32,verbose_name='用户名') pwd=models.CharField(max_length=32,verbose_name='密码') def __str__(self): return self.name class Meta: verbose_name_plural='用户表' class Book(models.Model): date=models.DateField(max_length=32,verbose_name='日期') time_choice=((1,"8:00"),(2,'9:00'),(3,'10:00'),(4,'11:00'),(5,'12:00'),(6,'13:00'), (7,'14:00'),(8,'15:00'),(9,'16:00'),(10,'17:00'),(11,'18:00'),(12,'19:00'), (13,'20:00'), ) time=models.IntegerField(choices=time_choice,verbose_name='时间段') room=models.ForeignKey('Room',verbose_name='会议室') user=models.ForeignKey('UserInfo',verbose_name='用户',null=True) class Meta: verbose_name_plural='预定表' unique_together=( ('date','time','room'), )
四、 主要业务
销售业务
销售业务说明(customer,SaleRank权重表 CustomerDistribution客户分配表)
一、目的
主要针对的是公司销售部门的工作管理,主要用于工作安排、销售进度的跟进以及为业绩考核提供数据支持。
二、业务机制
1.工作安排原则(分配订单原则)
根据对销售人员的以往业绩的分析制定每个销售人员的销售任务,综合多方面数据对销售人员进行权重的评定。在工作分配时,
通过权重进行排序,权重大的优先安排任务。分配任务时,按照权重排序从高到低顺位循环分配。(
若销售人员被分配任务以达到目标值则跳过,若所有销售人员都达到目标值则将剩余销售任务重复之前的方案进行分配)。
实现原理:
创建权重表,包含字段:user,权重,转化人数,创建一个类,获取权重表里面的所有销售人员并根据权重进行降序排序,根据权重和转换人数
将销售的id依次循环放入列表中,直到等于销售人员的转化人数就跳过该销售人员,在后台调用即可,当循环完以后,调用
reset()重置即可,主要使用Redis来完成(原因:减轻内存压力,普通放到内存中关机或断电就没了,这样不公平,Redis可以处理这个麻烦,其次Redis可以有效的处理多进程问题)
2.销售进度状态
销售人员得到任务后对应的客户状态改为开始接洽,记录起始时间,订单状态从公司资源更改为销售人员的个人资源,其他人在订单转移前不可接触订单信息。
销售人员在跟进订单时,每一次与客户接洽都会在数据库中生成一条记录。
若订单在十五日内被销售人员转化成功,则将该客户的状态由待转化变为转化成功,并在正式客户表中生成该客户的记录。在销售人员的订单记录中将这笔订单的状态改为转化成功。
若当前与客户接洽的销售人员三天未跟进订单或是在十五天内未促成交易。则相关订单信息会被移动到公司公共资源中,并且原先跟进订单的销售人员不可以选择继续跟进(直至该订单再次被移入公司公共资源)。原销售人员的订单跟进记录中会显示有一单未能转化,并显示原因(重新接手该订单后即使转化成功,本条记录不会被覆盖)。
实现原理:
a.客户来源:
1.来源一:运营部所分配的,权重分配一样(具体操作一样见上面)
2.来源二:抢单,从公司公共资源池里面获取:条件这个人不能是自己转化过de,或者是其他销售接单的但是接单超过15天
或超过3天没有联系的,点击抢单进行获取,将该用户添加到我的客户中(在客户分配表中创建一条新的记录),并且status默认
为正在根进,将该客户在客户表中的跟进人换成当前登录用户,接单时间和最近跟进时间换成当前时间即可
3.来源三:自己录入的
b.未转换成功(订单失效):当接单超过15天或者连续三天没有联系字该客户自动进入公共资源池,定时器完成
c.转换成功:转换成功后 加入我的客户中(在客户分配表中创建一条新的记录),并且status默认
为已转换
def public_view(self, request): """ 公共客户资源 :param request: :return: """ # 条件:未报名 status=2 并且 (15天未成单(当前时间-15 > 接客时间) or 3天未跟进(当前时间-3天>最后跟进日期) ) Q对象 # 方法一 # con = Q() # con1 = Q() # con1.children.append(('status', 2)) # # con2 = Q() # con2.connector = 'OR' # import datetime # now_date = datetime.datetime.now().date() # order_deadtime = now_date - datetime.timedelta(days=15) # talk_deadtime = now_date - datetime.timedelta(days=3) # # con2.children.append(('recv_date__lt', order_deadtime)) # con2.children.append(('last_consult_date__lt', talk_deadtime)) # # con.add(con1, 'AND') # con.add(con2, 'AND') # print(con, '------') # if con: # customers = models.Customer.objects.filter(con).all() # print(customers, '*****') # 方法二: current_user_id = 5 now_date = datetime.datetime.now().date() order_deadtime = now_date - datetime.timedelta(days=15) talk_deadtime = now_date - datetime.timedelta(days=3) customers = models.Customer.objects.filter( Q(recv_date__lt=order_deadtime) | Q(last_consult_date__lt=talk_deadtime), status=2).all() return render(request, 'public_customer_view.html', {"customers": customers, 'current_user_id': current_user_id})
def competition_view(self, request, cid): ''' 抢单 条件:必须原顾问不是自己 状态必须是未报名 并且 (15天未成单(当前时间-15 > 接客时间) or 3天未跟进(当前时间-3天>最后跟进日期) ) :param request: :param cid: 客户id :return: ''' current_usr_id = request.session['user'].get('id') now_date = datetime.datetime.now().date() order_deadtime = now_date - datetime.timedelta(days=15) talk_deadtime = now_date - datetime.timedelta(days=3) update_count = models.Customer.objects.filter( Q(recv_date__lt=order_deadtime) | Q(last_consult_date__lt=talk_deadtime), status=2, id=cid).exclude( consultant_id=current_usr_id).update(consultant_id=current_usr_id, last_consult_date=now_date, recv_date=now_date) if not update_count: return HttpResponse("抢单失败") models.CustomerDistribution.objects.create(user_id=current_usr_id, customer_id=cid, ctime=now_date) return HttpResponse('抢单成功')
3.销售数据
销售人员所接触过的每一个客户,不管什么来源,转换成功以否都会保存起来,为以后的权重划分和绩效考核为依据,
实现原理:
通过实现查看我的客户就可以一目了然的看到该销售人员的所有客户
def myuser_view(self, request): ''' 我的客户 :param request: :return: ''' current_user_id = request.session['user'].get("id") customers_list = models.CustomerDistribution.objects.filter(user_id=current_user_id).order_by('status') return render(request, 'myuser.html', {'customers': customers_list})
4.业绩考核
在销售人员的订单记录中记录了销售人员从第一笔业务到最近一笔业务的所有信息。可以为销售人员的业绩考核提供:接单数,转化率,订单未转化原因等数据。
学校管理业务说明 (courserecord,studyrecord)
一、系统用途
主要服务于教育机构,对教学班级,校区,课程,学生等进行管理,主要用于班级的成员管理、课堂出勤情况记录以及学员成绩记录。
二、业务机制
1.成员管理
销售人员与客户接洽完毕,将客户转化为学员后,根据其选择的校区、课程、班级将其信息录入学员的数据库中。初始化该学员的账号
和密码,以便其进入教学管理系统查看自己的成绩以及出勤记录。若该学员因某些原因中途退学或进入其他班级,则将其记录删除
(出勤记录与成绩记录详见2、3条说明)。
2.课堂出勤情况记录
每日上课前由班主任或当日讲师初始化当日的考勤信息,初始化时默认全部全员正常出勤。如有学员存在:迟到、旷到、早退或请假等情况。可由班主任或当日讲师修改其考勤状况(支持批量修改)。
若有学生中途进入班级,进班前的考勤记录不与不予生成。若有学生中途离开教学班级,离班前的考勤记录不予删除。
上课教师和班主任对学生进行考勤管理,考勤直接影响这节课的成绩,考勤种类为已签到,迟到,缺勤,早退,请假.
初始化实现原理:
点击复选框选中要初始化的当天班级课程,点击action中学生初始化对学生完成初始化 默认全部出勤,
def multu_init(self,request): id_list=request.POST.getlist('pk') CourseRecord_list=models.CourseRecord.objects.filter(id__in=id_list).all() for courseRecord in CourseRecord_list: exists=models.StudyRecord.objects.filter(course_record=courseRecord).exists() if exists: continue class_obj=courseRecord.class_obj student_list=models.Student.objects.filter(class_list=class_obj) studyrecord_list=[] for student in student_list: studyrecord= models.StudyRecord(course_record=courseRecord,student=student) studyrecord_list.append(studyrecord) models.StudyRecord.objects.bulk_create(studyrecord_list) multu_init.short_desc='学生初始化' action_func_list = [multu_init,multi_del]
如果有个别学生出现违规情况,在studyrecord中对该学生进行操作
实现原理:
教师和班主任在课程记录页面点击考勤管理,调转到该班级的学习记录页面,列出该班级的所有学生,利用action对学生进行批量的
考勤管理
#考勤 def CheckWa(self,obj=None,is_head=False): if is_head: return '考勤' url='/stark/app01/studyrecord/?course_record=%s'%obj.id return mark_safe("<a href='%s'>考勤管理</a>"%url) def checked(self,request): id_list=request.POST.getlist('pk') models.StudyRecord.objects.filter(id__in=id_list).update(record='checked') checked.short_desc='已签到' def vacate(self, request): id_list = request.POST.getlist('pk') models.StudyRecord.objects.filter(id__in=id_list).update(record='vacate') vacate.short_desc = '请假' def late(self,request): id_list = request.POST.getlist('pk') models.StudyRecord.objects.filter(id__in=id_list).update(record='late') late.short_desc='迟到' def noshow(self, request): id_list = request.POST.getlist('pk') models.StudyRecord.objects.filter(id__in=id_list).update(record='noshow') noshow.short_desc = '缺勤' def leave_early(self,request): id_list = request.POST.getlist('pk') models.StudyRecord.objects.filter(id__in=id_list).update(record='leave_early') leave_early.short_desc='早退' show_action = True action_func_list = [checked,vacate,late,noshow,leave_early]
3.学员成绩记录
在班主任或当日讲师进行初始化考勤信息操作时可以选择当日是否有作业(支持修改)。若当日有作业则开放学生作业提交的功能,
学生须在提交时间内提交,提交时间结束关闭该功能。
学生提交作业后在提交时间内允许撤销提交并重新提交。提交时间结束后,助教可以下载学员提交文件,并进行打分评定。
打分和评定评语结束后可立即上传至教学管理系统,学生可以通过教学管理系统进行查询(支持批量导入)。
若有学生中途进入班级,进班前的成绩记录不与不予生成。若有学生中途离开教学班级,离班前的成绩记录不予删除。
成绩录入实现原理:
在课程记录页面点击成绩录入调到成绩录入页面,主要是根据当前趁机记录的id获取该节课所有的学生记录对象,发送dao前端,
其中由于成绩打分要严格区分,所欲成绩和评语有type动态生成ModelForm对象传到前端,fields字段分别是score_学生记录id
,homework_note_学生记录对象id,post传到后端 的时候在courseRecordConfig中看代码成绩录入
#录入成绩 def inputScore(self,request,cr_id): if request.method=='GET': studyRecord_list=models.StudyRecord.objects.filter(course_record_id=cr_id) data=[] for studyRecord in studyRecord_list: #动态生成form SRForm=type('SRForm',(Form,),{ 'score_%s'%studyRecord.id : fields.ChoiceField(models.StudyRecord.score_choices), 'homework_note_%s'%studyRecord.id :fields.ChoiceField(widget=widgets.Textarea(attrs={"style":'395px;height:45px'})) }) data.append({"studyRecord":studyRecord,"form":SRForm(initial={'score_%s'%studyRecord.id:studyRecord.score,'homework_note_%s'%studyRecord.id:studyRecord.homework_note})}) return render(request,'inputScore.html',{'data':data}) else: data=request.POST print(data) for k,v in data.items(): if k=='csrfmiddlewaretoken': continue name,studyRecord_id=k.rsplit('_',1) data_dic={} if studyRecord_id in data_dic: data_dic[studyRecord_id][name]=v else: data_dic[studyRecord_id]={name:v} for id,dic in data_dic.items(): models.StudyRecord.objects.filter(id=id).update(**dic) return redirect(self.get_list_url())
{% extends 'stark/base.html' %} {% block body %} <h1>录入学生成绩</h1> <form action="" method="post"> {% csrf_token %} <table class="table table-bordered"> <tr> <th>课程</th> <th>学生姓名</th> <th>出勤</th> <th>成绩</th> <th>评语</th> </tr> {% for foo in data %} <tr> <td>{{ foo.studyRecord.course_record }}</td> <td>{{ foo.studyRecord.student }}</td> <td>{{ foo.studyRecord.get_record_display }}</td> {% for field in foo.form %} <td> {{ field }}</td> {% endfor %} </tr> {% endfor %} </table> <input type="submit" value="保存" class="btn btn-primary"> </form> {% endblock %}
会议室预定 业务
公司人员增多,空间有限,会议室需要被预定才可使用,每个会议室从早上八点-晚上八点可以预定,一小时划分,如果该会议室当前时间被预定了,如果预定的人是自己,再点击则取消,如果是别人预定的则不可以点击
,如果没有没预定点击则预定,
调查问卷 业务
调查问卷是为了调查学生对学校设备和讲师讲课的满意程度,以及他没有什么困难等,获取他们的意见以方便我们进行改进,问卷只能有班主任和教务总监发起,并明确的规定班级和起始日期和结束时间,并且只有本班学生才能填写,调查问卷的题型有三种,填写内容(建议),单选,打分。学生打分后点击提交即可完成调查问卷,学生提交后,在首页可显示答卷的人数。
技术点:
1. 通过ChangeList封装好多数据
原因:
change_listview列表页面代码太多啦 而却有好几个功能,代码结构不清晰,修改代码麻烦,传到前端的东西过多,封装后代码结构清晰,解耦性提高,便于维护,而且在changelist_view中只需要传入changelist对象即可。
2. 销售中公共资源:Q查询,3天 15天
销售接单后开始记录时间,如果三天 未跟进十五天未成单,该客户进入公司的公共资源池,并且当前销售人员不能在公共资源池里面对该客户没有任何权限.
def public_view(self, request): """ 公共客户资源 :param request: :return: """ # 条件:未报名 status=2 并且 (15天未成单(当前时间-15 > 接客时间) or 3天未跟进(当前时间-3天>最后跟进日期) ) Q对象 # 方法一 # con = Q() # con1 = Q() # con1.children.append(('status', 2)) # # con2 = Q() # con2.connector = 'OR' # import datetime # now_date = datetime.datetime.now().date() # order_deadtime = now_date - datetime.timedelta(days=15) # talk_deadtime = now_date - datetime.timedelta(days=3) # # con2.children.append(('recv_date__lt', order_deadtime)) # con2.children.append(('last_consult_date__lt', talk_deadtime)) # # con.add(con1, 'AND') # con.add(con2, 'AND') # print(con, '------') # if con: # customers = models.Customer.objects.filter(con).all() # print(customers, '*****') # 方法二: current_user_id = 5 now_date = datetime.datetime.now().date() order_deadtime = now_date - datetime.timedelta(days=15) talk_deadtime = now_date - datetime.timedelta(days=3) customers = models.Customer.objects.filter( Q(recv_date__lt=order_deadtime) | Q(last_consult_date__lt=talk_deadtime), status=2).all() return render(request, 'public_customer_view.html', {"customers": customers, 'current_user_id': current_user_id})
3. 使用yield实现(前端需要循环生成数据时)
- 生成器函数,对数据进行加工处理
- __iter__和yield配合
组合搜索时用到先在ChangList中的get_combine_seach_filter()中返回row对象,然后在FilterRow类中创建__iter__方法 yied生成每个组合搜索所对应的url
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=[]代表可以以什么搜索
# 定制 search_fileds = [] def get_search_fileds(self): data = [] if self.search_fileds: data.extend(self.search_fileds) return data def get_search_condition(self): key_word = self.request.GET.get(self.search_key) search_fileds = self.get_search_fileds() condition = Q() condition.connector = 'or' if key_word and self.get_show_search_form(): for filed in search_fileds: condition.children.append((filed, key_word)) return condition
# 是否显示搜索框 self.show_search_form = config.get_show_search_form() # 搜索框的value和url同步 self.search_form_val = self.request.GET.get(config.search_key, ' ')
{% if cl.show_search_form %} <form method="get" style="padding-left:0;float: right"> <div class="form-group" style="display: inline-block"> <input type="text" name="{{ cl.config.search_key }}" value="{{ cl.search_form_val }}" class="form-control" placeholder="Search" style="200px"> </div> <button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button> </form> {% endif %}
6. Type创建类
主要用于动态生成modelForm时用到,在调查问卷和成绩录入是用到
Type中第一个参数是类名,第二个是继承的类,第三个是字典,其中我们操作主要是在字典中进行操作
#录入成绩 def inputScore(self,request,cr_id): if request.method=='GET': studyRecord_list=models.StudyRecord.objects.filter(course_record_id=cr_id) data=[] for studyRecord in studyRecord_list: #动态生成form SRForm=type('SRForm',(Form,),{ 'score_%s'%studyRecord.id : fields.ChoiceField(models.StudyRecord.score_choices), 'homework_note_%s'%studyRecord.id :fields.ChoiceField(widget=widgets.Textarea(attrs={"style":'395px;height:45px'})) }) data.append({"studyRecord":studyRecord,"form":SRForm(initial={'score_%s'%studyRecord.id:studyRecord.score,'homework_note_%s'%studyRecord.id:studyRecord.homework_note})}) return render(request,'inputScore.html',{'data':data}) else: data=request.POST print(data) for k,v in data.items(): if k=='csrfmiddlewaretoken': continue name,studyRecord_id=k.rsplit('_',1) data_dic={} if studyRecord_id in data_dic: data_dic[studyRecord_id][name]=v else: data_dic[studyRecord_id]={name:v} for id,dic in data_dic.items(): models.StudyRecord.objects.filter(id=id).update(**dic) return redirect(self.get_list_url())
7. 自动派单
- 原来在内存中实现,问题:重启和多进程时,都有问题。
- redis
- 状态
- 原来数据(权重表 权重和个数)
- pop数据
上面 业务中有具体体说明
8. 使用 list_diplay配置
list_display = [函数名,字段名。。。。]
9. reverse反向生成URL
根据url中name字段的值利用reverse生成,如果有namespace则需要在最前面加上,并用“:””分隔,url中有参数还需要传参args=[]
# 总的url def get_urls(self): model_class_app = (self.model_class._meta.app_label, self.model_class._meta.model_name) urlpatterns = [ url(r'^$', self.wrap(self.changlist_view), name='%s_%s_changelist' % model_class_app), url(r'^add/$', self.wrap(self.add_view), name='%s_%s_add' % model_class_app), url(r'^(d+)/change/$', self.wrap(self.chang_view), name='%s_%s_change' % model_class_app), url(r'^(d+)/delete/$', self.wrap(self.delete_view), name='%s_%s_delete' % model_class_app) ] urlpatterns.extend(self.extra_url()) return urlpatterns # 额外的url 在自己的config中定义该函数添加 def extra_url(self): return [] # 列表页面的url def get_list_url(self): name = 'stark:%s_%s_changelist' % (self.model_class._meta.app_label, self.model_class._meta.model_name) list_url = reverse(name) return list_url # 添加按钮的url def get_add_url(self): name = 'stark:%s_%s_add' % (self.model_class._meta.app_label, self.model_class._meta.model_name) add_url = reverse(name) return add_url # 编辑的url def get_change_url(self, nid): name = 'stark:%s_%s_change' % (self.model_class._meta.app_label, self.model_class._meta.model_name) edit_url = reverse(name, args=(nid,)) return edit_url # 删除的url def get_delete_url(self, nid): name = 'stark:%s_%s_delete' % (self.model_class._meta.app_label, self.model_class._meta.model_name) del_url = reverse(name, args=(nid,)) return del_url
10. 母版
模板的继承
模板中 {%block body%}{%endblock%}
子版中最顶行{% extends '母版的路径' %}
{%block body%}{%endblock%}
11. ready方法定制起始文件
- 文件导入实现单例模式
from django.apps import AppConfig class StarkConfig(AppConfig): name = 'stark' def ready(self): from django.utils.module_loading import autodiscover_modules autodiscover_modules('stark')
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源码可知,中间件也是采用这种方法
MESSAGE_CLASSES = [ 'stark.utils.message.msg.Msg', 'stark.utils.message.wx.WeChat', 'stark.utils.message.dingding.DingDing', 'stark.utils.message.email.Email', ]
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的循环
# ''' # for bfield in form: # print(bfield.field, '--------------', type(bfield.field)) # <django.forms.fields.CharField object at 0x00000179480E6F60> -------------- <class 'django.forms.fields.CharField'> # <django.forms.fields.CharField object at 0x00000179480E6FD0> -------------- <class 'django.forms.fields.CharField'> # <django.forms.fields.TypedChoiceField object at 0x00000179480F5048> -------------- <class 'django.forms.fields.TypedChoiceField'> # <django.forms.models.ModelChoiceField object at 0x00000179480F50B8> -------------- <class 'django.forms.models.ModelChoiceField'> # <django.forms.models.ModelMultipleChoiceField object at 0x00000179480F5128> -------------- <class 'django.forms.models.ModelMultipleChoiceField'> # ''' # # # '''' # #bfield.field.queryset # <QuerySet [<Department: 教育部>, <Department: 销售部>]> # <QuerySet [<Role: 老师>, <Role: 学生>]> # ''' # # ''' # #bfield.field.queryset.model # <class 'app03.models.Department'> # <class 'app03.models.Role'> # ''' # # ''' # #bfield.auto_id # id_depart # id_roles # '''
for bfield in model_obj_form: temp = {"is_popup": False, "bfiled": bfield} from django.forms import ModelChoiceField #判断是不是外键和多对多 if isinstance(bfield.field, ModelChoiceField): #获取相关联的model类 relate_class_name = bfield.field.queryset.model #判断当前获取的model类是否注册了 if relate_class_name in site._registry: #app名称和modellei名称 app_model_name = relate_class_name._meta.app_label, relate_class_name._meta.model_name #获取当前获取model类add的url baseurl = reverse('stark:%s_%s_add' % app_model_name) model_name=config.model_class._meta.model_name related_name=config.model_class._meta.get_field(bfield.name).rel.related_name print(related_name,'related_name',type(related_name),'------------') #构建popup的url popurl = '%s?_popbackid=%s&model_name=%s&related_name=%s' % (baseurl, bfield.auto_id,model_name,related_name) temp["is_popup"] = True temp['popurl'] = popurl
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字段