DC's blog DC's blog
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)

DC

愿我一生欢喜,不为世俗所及.
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)
  • BBS

  • 订单平台

  • CRM

    • rbac

    • stark

    • crm

      • 基础业务处理
        • CRM介绍
          • 项目背景
          • 项目开发概览
        • 准备工作
        • 校区管理|部门管理
        • 代码拆分
        • 用户管理
          • CURD
          • 重置密码
          • 使用Form组件
          • 使用ModelForm组件
        • 课程管理
        • 班级管理
          • 数据表设计
          • 表格数据展示
          • 新增和编辑
        • ■ 需求:所教授课程
      • 客户管理
      • 学员管理
      • 权限应用
  • flask+layui

  • django+layui

  • 供应链

  • 实战
  • CRM
  • crm
DC
2024-03-28
目录

基础业务处理

# CRM介绍

该crm系统以教育机构为背景. 主要为 销售部、运营部、教质部 提供管理平台. 通过系统来约束员工的行为!
官方点: 随着公司规模的扩展, CRM系统对公司员工的业务信息量化以及信息化建设越来越重要!

# 项目背景

crm系统为不同角色(销售、运营、教质)的用户提供了不同的功能

■ 销售部

客户分为公户和私户
- 公户,公共客户(通过广告、转介绍、上门咨询..等渠道获得的客户)
- 私户,销售自己的客户
1> "公户转换成私户"
   所有销售都能看到公户的信息,想要将公户转换成私户,需要进行申请
   申请将公户放到自己的私户里 (Ps:这样哪怕其他人临门一脚,把客户抢了促成了这一单,业绩也会算作是自己的!)
2> "私户分配限制"
   每个销售的私户个数有限制,一般 <=150个客户
   只有当自己的私户个数小于规定客户数时,才能去公户里转换客户成自己的私户!
   所以,当销售跟某个客户聊天跟进一段时间后,觉得没戏,就可以将该客户 踢到 公户中,这样就能申请公户到自己私户中了.
3> "跟进记录"
   销售需要填写跟进记录,一方面是为了方便隔一段时间后继续跟进客户;另一方面是放弃后,踢到公户中,能让其他想转换的销售了解信息.
4> "添加入班申请"
   当客户转化成功之后,要添加入班申请且缴费信息,最终由财务审核入班
1
2
3
4
5
6
7
8
9
10
11
12
13
14

■ 运营部

在该系统中运营部的主要工作是,录入客户信息到公户并对于客户进行跟踪!
在真实的工作场景中,运营部还需要进行数据分析等工作.
1
2

■ 教质部

- 日常学员考勤及上课记录
- 定时对学员进行谈话
- 纪律维护(关乎积分管理)
- 可由班主任可以发起转班以及留级申请(定期考核不通过就得留级)
1
2
3
4

# 项目开发概览

先对项目的开发有个概览 (´・Д・)」

- 基础业务处理
  - 校区管理(北京、上海、深圳、广州)
  - 部门管理(销售、运营、教质)
  - 用户管理
  - 课程管理
  - 开班管理
- 客户管理
  - 公户
  - 私户
  - 缴费和报名申请
- 学员管理
  - 学生管理
  - 考勤
  - 谈话记录
  - 积分
- 应用rbac组件,进行权限的控制
1
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是不会生成表的!!
1
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),
]
1
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
1
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)
1
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/

image-20240328194718882

至此, Department表的CURD完成!! 访问路由: http://127.0.0.1:8000/stark/web/school/list/

image-20240328200937456

其他:
1.注意 多app的模版查找顺序
2.在原来的stark组件基础上, 增加了 默认添加 "操作"一列的代码!(即 编辑|删除 放在了一起
1
2
3

# 代码拆分

按照下面的代码结构, 更加的清晰!!

image-20240401161532788


# 用户管理

(*≧ω≦)

■ 用户添加和用户编辑需要定义不同的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!!
1
2
3
4
5
6
7
8
9
10
11
12

# CURD

(・_・;

关键代码如下:

image-20240401160229295

其他:
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中不会有该字段!!源码可考究!
1
2
3
4
5
6
7
8
9
10
11

# 重置密码

╮( ̄▽ ̄"")╭

用户管理页面新增一列"重置密码"、在原有的CURD的url上新增"重置密码"的url, 编写对应的视图函数和form表单..

image-20240401182457702

# 使用Form组件

重置密码页面 只有两个字段 "密码"、"确认密码"! 也无需填充原来的密码的值, 展示页面 也跟ORM数据库的字段没啥关系!
所以我们 使用 Form组件 来实现!!

关键代码如下:

image-20240401173942073

其他:
1. 在原来的stark组件基础上,对标StarkModelForm添加了一个父类 StarkForm..
1
2
# 使用ModelForm组件

在重置密码的页面, 用户名字段是不能编辑的. - 用 readonly 和 initial 来实现的!!
结合 rbac部分 - 菜单和权限管理.md - 用户管理小节 的笔记 进行理解!!

read_only的详细解释参考: rbac组件中菜单和权限管理.md中的用户管理里的内容!!

关键代码如下:

image-20240401175511534

Ps;截图中有一处代码不规范!

form = ResetPasswordForm(instance=userinfo_object, data=request.POST, initial={'name': userinfo_object.name})
应改为:
form = ResetPasswordForm(instance=userinfo_object, data=request.POST)

因为,官方文档是这么说的:
  `initial` 值在验证中 *不* 用作“后备”数据, 如果某个字段的值没有给出. `initial` 值 *仅* 用于初始表格显示.
1
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参数,不好修改源码!!
    (・_・; 其实想来想去最好的就是重写该路由该视图函数!!简单快捷!没错就这么干!! 不改源码啦!
1
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
1
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)
1
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"]
1
2
3
4
5

至此, Course课程表的CURD完成!! 访问路由: http://127.0.0.1:8000/stark/web/course/list/

image-20240401181144806


# 班级管理

(*≧ω≦)

# 数据表设计

先来看看数据表是怎么设计的!

关键代码如下:

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}期)"
1
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 用于反向生成!

# 表格数据展示

(*≧ω≦)

image-20240402145818454

其他:
1. 在原来的stark组件基础上,添加了处理时间日期格式的闭包
2. 在原来的stark组件基础上,添加了处理多对多文本信息的闭包
1
2
3

# 新增和编辑

(*≧ω≦)

先来看看效果

49

关键代码如下:

image-20240402194122360

★ 注意: 我们规定在新增和编辑时, 班主任得是教质部的人, 任课老师得是教学部的人!

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__中处理(推荐!!)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# ■ 需求:所教授课程

在班级管理页面,我们可以看到 该课程有哪些老师教!! 那么在用户管理界面, 我们想看到, 教师部的用户教了哪些课程!!
要实现这个需求, 关键在于理解 多对多的反向查找!!

先来看看效果

image-20240402220220029

首先,我先写了个脚本看能不能拿到自己想要的数据!

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"))
1
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]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

搜索相关
客户管理

← 搜索相关 客户管理→

最近更新
01
deepseek本地部署+知识库
02-17
02
实操-微信小程序
02-14
03
教学-cursor深度探讨
02-13
更多文章>
Theme by Vdoing | Copyright © 2023-2025 DC | One Piece
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式