介绍

CRM开发(系统)

CRM,客户关系管理系统(Customer Relationship Management)。企业用CRM技术来管理与客户之间的关系,以求提升企业成功的管理方式,其目的是协助企业管理销售循环:新客户的招徕、保留旧客户、提供客户服务及进一步提升企业和客户的关系,并运用市场营销工具,提供创新式的个人化的客户商谈和服务,辅以相应的信息系统或信息技术如数据挖掘和数据库营销来协调所有公司与顾客间在销售、营销以及服务上的交互。

此系统主要是以教育行业为背景,为公司开发的一套客户关系管理系统。考虑到各位童鞋可能处于各行各业,为了扩大的系统使用范围,特此将该项目开发改为组件化开发,让同学们可以日后在自己公司快速搭建类似系统及新功能扩展。系统分为三部分:1. 权限系统,一个独立的rbac组件(权限);2. stark组件,一个独立的curd组件(增删改查);3. crm业务,以教育行业为背景并整合以上两个组件开发一套系统。

 

项目截图:

crm_resource切换资源组 crm组件_django

crm_resource切换资源组 crm组件_crm_resource切换资源组_02

crm_resource切换资源组 crm组件_django_03

 

crm系统包含:

  权限组件(rbac)(通用在任何系统都可以用)

  stark业务(增删改查)

  crm业务(将权限组件stark组件应用到此)

 在web程序中什么是权限?

一个权限就是一个url(只有访问url才能访问某功能)

结论:一个人有多少个权限就取决于他有多少个URL的访问权限。

表结构设计

第一版:

问答环节中已得出权限就是URL的结论,那么就可以开始设计表结构了。

  • 一个用户可以有多个权限。
  • 一个权限可以分配给多个用户。

你设计的表结构大概会是这个样子:

crm_resource切换资源组 crm组件_crm_resource切换资源组_04

crm_resource切换资源组 crm组件_crm_resource切换资源组_05

crm_resource切换资源组 crm组件_中间件_06

But,无论是是否承认,你还是too young too native,因为老汉腚眼一看就有问题....

问题:假设 “老男孩”和“Alex” 这俩货都是老板,老板的权限一定是非常多。那么试想,如果给这俩货分配权限时需要在【用户权限关系表中】添加好多条数据。假设再次需要对老板的权限进行修改时,又需要在【用户权限关系表】中找到这俩人所有的数据进行更新,太他妈烦了吧!!! 类似的,如果给其他相同角色的人来分配权限时,必然会非常繁琐。

第二版

角色表、权限表和用户表 

角色表和权限表多对多 角色表和用户表多对多

另外还有角色和权限的关系表 角色和用户的关系表

聪明机智的一定在上述的表述中看出了写门道,如果对用户进行角色的划分,然后对角色进行权限的分配,这不就迎刃而解了么。

  • 一个人可以有多个角色。
  • 一个角色可以有多个人。
  • 一个角色可以有多个权限。
  • 一个权限可以分配给多个角色。

表结构设计:

crm_resource切换资源组 crm组件_当前用户_07

crm_resource切换资源组 crm组件_中间件_08

crm_resource切换资源组 crm组件_当前用户_09

crm_resource切换资源组 crm组件_中间件_10

crm_resource切换资源组 crm组件_当前用户_11

 这次调整之后,由原来的【基于用户的权限控制】转换成【基于角色的权限控制】,以后再进行分配权限时只需要给指定角色分配一次权限,给众多用户再次分配指定角色即可。

一个经典的权限访问控制系统:rbac(Role-Based Access Control)基于角色的权限访问控制

创建项目

1、创建django project, luffy_permission

django-admin startproject luffy_permission

2、创建两个app

  rbac 权限组件

  web 销售管理系统

python manage.py startapp rbac
python manage.py startapp web

 

创建表结构:

3、app :rbac(基于角色的权限控制系统)  将权限相关的表编写到此app的models.py中

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = 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)

    def __str__(self):
        return self.name

View Code

4、app:web(客户管理系统)   将销售系统表写到models.py

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

from django.db import models


class Customer(models.Model):
    """
    客户表
    """
    name = models.CharField(verbose_name='姓名', max_length=32)
    age = models.CharField(verbose_name='年龄', max_length=32)
    email = models.EmailField(verbose_name='邮箱', max_length=32)
    company = models.CharField(verbose_name='公司', max_length=32)


class Payment(models.Model):
    """
    付费记录
    """
    customer = models.ForeignKey(verbose_name='关联客户', to='Customer')
    money = models.IntegerField(verbose_name='付费金额')
    create_time = models.DateTimeField(verbose_name='付费时间', auto_now_add=True)

web/models.py

View Code

数据迁移:

python manage.py makemigrations
python manage.py migrate

接下里下来我们用《客户管理》系统web为示例,提出功能并实现 

目录结构:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
luffy_permission/
├── db.sqlite3
├── luffy_permission
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── rbac            # 权限组件,便于以后应用到其他系统
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── templates
└── web            # 客户管理业务
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    └── views.py

View Code

《客户管理》系统截图:

crm_resource切换资源组 crm组件_中间件_18

crm_resource切换资源组 crm组件_django_19

crm_resource切换资源组 crm组件_当前用户_20

 

销售管理系统业务

销售管理系统中的URL有:

  • 客户管理
  • 客户列表:/customer/list/
  • 添加客户:/customer/add/
  • 删除客户:/customer/list/(?P<cid>\d+)/
  • 修改客户:/customer/edit/(?P<cid>\d+)/
  • 批量导入:/customer/import/
  • 下载模板:/customer/tpl/
  • 账单管理
  • 账单列表:/payment/list/
  • 添加账单:/payment/add/
  • 删除账单:/payment/del/(?P<pid>\d+)/
  • 修改账单:/payment/edit/<?P<pid>\d+/

销售管理系统权限录入

那么接下来,以下操作都是基于admin手动操纵权限组件中录入相关信息:

  • 录入权限(url)
  • 创建用户
  • 创建角色
  • 用户分配角色
  • 角色分配权限
# 录入超级用户(test 123 用来登录admin)
python3 manage.py createsuperuser

将rbac中的表加入到admin中用来录入信息到数据库中

rbac/admin.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

from django.contrib import admin
from rbac import models

admin.site.register(models.Permission)
admin.site.register(models.UserInfo)
admin.site.register(models.Role)

View Code

 

crm_resource切换资源组 crm组件_当前用户_23

 

 快速实现简单的权限控制

这么一来,用户登录时,就可以根据自己的【用户】找到所拥有的角色,再根据角色找到所拥有的权限,再将权限信息放入session,以后每次访问时候需要先去session检查是否有权访问。

至此,基本的权限控制基本流程为:

  • 用户登录,获取权限信息并放入session
  • 用户访问,在中间件从session中获取用户权限信息,并进行权限验证。

1、登录页面是否有权限访问
2、POST请求 用户登录检验是否合法
3、获取当前用户相关的所有权限(url)并放入session中
4、再次向服务端发起请求 http://www/xxx.com/xxx,后端编写中间件对用户当前访问的url进行权限的判断(是否在session中)

 

web系统开始开发:

1、用户登录(POST请求 用户登录检验是否合法):

2、获取当前用户相关的所有权限(url)并放入session中:

urls.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

from django.conf.urls import url
from web.views import customer
from web.views import payment
from web.views import account

urlpatterns = [
    url(r'^customer/list/$', customer.customer_list),
    url(r'^customer/add/$', customer.customer_add),
    url(r'^customer/edit/(?P<cid>\d+)/$', customer.customer_edit),
    url(r'^customer/del/(?P<cid>\d+)/$', customer.customer_del),
    url(r'^customer/import/$', customer.customer_import),
    url(r'^customer/tpl/$', customer.customer_tpl),

    url(r'^payment/list/$', payment.payment_list),
    url(r'^payment/add/$', payment.payment_add),
    url(r'^payment/edit/(?P<pid>\d+)/$', payment.payment_edit),
    url(r'^payment/del/(?P<pid>\d+)/$', payment.payment_del),

    url(r'^login/$', account.login), # 登录

]

View Code

视图account.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django.shortcuts import HttpResponse, render,redirect

from rbac import models


def login(request):
    if request.method == 'GET':
        # templates模板 在django中的查找顺序 先在主目录下查 没有在按app的注册顺序在app里查
        return render(request, 'login.html')
    # 获取请求的用户名和密码
    user = request.POST.get('user')
    pwd = request.POST.get('pwd')
    # 验证登录用户
    current_user = models.UserInfo.objects.filter(name=user, password=pwd).first()
    if not current_user:
        return render(request, 'login.html', {'msg': '用户名或密码错误'})

    # 根据当前用户信息获取此用户所拥有的所有权限,并放入session。

    # 当前用户所有权限(跨表查询 去重(一个用户又多个角色 一个角色有多个权限 那么一个用户会有重复的权限)角色权限为空)
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__id",
                                                                                      "permissions__url").distinct()

    # 获取权限中所有的URL
    # permission_list = []
    # for item in permission_queryset:
    #     permission_list.append(item['permissions__url'])

    permission_list = [item['permissions__url'] for item in permission_queryset]
    # 设置session
    request.session['luffy_permission_url_list_key'] = permission_list
    # 登录成功重定向
    return redirect('/customer/list/')

View Code

注意点:

# templates模板 在django中的查找顺序 先在主目录下查 没有在按app的注册顺序在app里查

# 当前用户所有权限(跨表查询 去重(一个用户又多个角色 一个角色有多个权限 那么一个用户会有重复的权限)角色权限为空)

 # 设置session   # 登录成功重定向

 

模板login.html:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post">
    {% csrf_token %}
    <input type="text" name="user" placeholder="请输入用户名">
    <input type="password" name="pwd" placeholder="请输入密码">
    <input type="submit" value="提交"><span style="color: red">{{ msg }}</span>
</form>

</body>
</html>

View Code

 

3、再次向服务端发起请求 http://www/xxx.com/xxx,后端编写中间件对用户当前访问的url进行权限的判断(是否在session中)

编写中间件:

注意点:

  path_info取的当前url,session取值,re.match正则匹配,中间件的继承

  登录页等这些所有人都登的面加入白名单(# 中间件中return None中间件不拦 可以继续往下执行)

web/md/xxx.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse

# 不知道中间件继承谁那就随便找一个中间件继承它就行了
class CheckPermission(MiddlewareMixin):
    """
    用户权限信息校验
    """

    def process_request(self, request):
        """
        当用户请求刚进入时候出发执行
        :param request:
        :return:
        """

        """
        1. 获取当前用户请求的URL
        2. 获取当前用户在session中保存的权限列表 ['/customer/list/','/customer/list/(?P<cid>\\d+)/']
        3. 权限信息匹配
        """
        # 白名单
        valid_url_list = [
            '/login/',
            '/admin/.*'
        ]
        # 1. 获取当前用户请求的URL
        # path_info获取当前用户访问的url 记下就行了
        current_url = request.path_info
        for valid_url in valid_url_list:
            if re.match(valid_url, current_url):
                # 白名单中的URL无需权限验证即可访问
                # 中间件中return None中间件不拦 可以继续往下执行
                return None
                
        # 2. 获取当前用户在session中保存的权限列表
        permission_list = request.session.get('luffy_permission_url_list_key')
        if not permission_list:
            return HttpResponse('未获取到用户权限信息,请登录!')

        flag = False
        
        # 3. 权限信息匹配 正则
        for url in permission_list:
            # 正则的起始^终止符$(不能无限的匹配随便写点都能匹上)
            reg = "^%s$" % url
            if re.match(reg, current_url):
                flag = True
                break

        if not flag:
            return HttpResponse('无权访问')

View Code

注册中间件settings.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

..........

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'web.md.xxx.CheckPermission'
]
...............

View Code

 

功能完善 将权限相关的的功能放到rbac应用下(封装) 以便于以后组件的应用

(将权限初始化封装到rbac中,将权限中间件移到rbac中,将一些常量放到配置文件中)

用户登录和权限初始化拆分:

rbac/service/init_permission.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django.conf import settings


def init_permission(current_user, request):
    """
    用户权限的初始化
    :param current_user: 当前用户对象
    :param request: 请求相关所有数据
    :return:
    """
    # 2. 权限信息初始化
    # 根据当前用户信息获取此用户所拥有的所有权限,并放入session。
    # 当前用户所有权限
    permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__id",
                                                                                      "permissions__url").distinct()

    # 获取权限中所有的URL
    # permission_list = []
    # for item in permission_queryset:
    #     permission_list.append(item['permissions__url'])

    permission_list = [item['permissions__url'] for item in permission_queryset]
    request.session[settings.PERMISSION_SESSION_KEY] = permission_list

View Code

web/views/account.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django.shortcuts import HttpResponse, render, redirect
from rbac import models
from rbac.service.init_permission import init_permission


def login(request):
    # 1. 用户登录
    if request.method == 'GET':
        return render(request, 'login.html')
    user = request.POST.get('user')
    pwd = request.POST.get('pwd')

    current_user = models.UserInfo.objects.filter(name=user, password=pwd).first()
    if not current_user:
        return render(request, 'login.html', {'msg': '用户名或密码错误'})

    init_permission(current_user, request)

    return redirect('/customer/list/')

View Code

中间件转移至rbac的middlewares文件中并修改settings文件中的中间件位置

配置文件应用:(配置文件的key不能小写)

settings.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

# ######################### 权限相关配置 ############################

PERMISSION_SESSION_KEY = "luffy_permission_url_list_key"
VALID_URL_LIST = [
    '/login/',
    '/admin/.*'
]

View Code

 

动态菜单 (一级二级)

菜单是什么:首先有标题 还得有url能跳转(和权限一样了)

什么能做菜单什么不能(类似什么列表的可以,添加删除的不行)

权限和菜单就是包含关系

 

如何实现动态显示一级菜单:

浏览器登录发送post请求,获取当前用户相关的所有权限(url)和菜单信息并放入session中,

再次向服务端发起请求 http://www/xxx.com/xxx,后端编写中间件对用户当前访问的url进行权限的判断(是否在session中),

菜单信息不在中间件中做判断,在视图中在session中做判断,在模板中把是菜单的信息展示出来

 

 1、修改数据库表(添加栏是否能做菜单字段和对应的图标字段)

图标网(图标的使用):https://fontawesome.com.cn/

rbac/models.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)
    
    # https://fontawesome.com.cn/ 图标网
    icon = models.CharField(verbose_name='图标', max_length=32, null=True, blank=True, help_text='菜单才设置图标')
    is_menu = models.BooleanField(verbose_name='是否是菜单', default=False)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = 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)

    def __str__(self):
        return self.name

View Code

数据迁移:

python manage.py makemigrations
python manage.py migrate

添加数据 :

将需要展示为菜单的is_menu字段置为true

将图标名称添加到icon字段中

2、获取权限和菜单信息并保存到session中(初始化权限信息)

raac/service/init_permission.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django.conf import settings


def init_permission(user, request):
    """
    用户权限初始化
    :param user:
    :param request:
    :return:
    """

    # 根据角色获取所有权限和菜单及图标
    permission_list = user.roles.filter(permissions__id__isnull=False).values('permissions__id',
                                                                              'permissions__title',
                                                                              'permissions__url',
                                                                              'permissions__is_menu',
                                                                              'permissions__icon'
    
                                                                          ).distinct()
    # 获取权限中所有的url和菜单信息
    menu_list = []
    permission_url_list = []
    for item in permission_list:
        if item['permissions__is_menu']:
            tmp = {
                'title': item['permissions__title'],
                'icon': item['permissions__icon'],
                'url': item['permissions__url']
            }
            menu_list.append(tmp)

        permission_url_list.append(item['permissions__url'])

    # 将权限信息和菜单信息 放入session
    request.session[settings.MENU_SESSION_KEY] = menu_list
    request.session[settings.PERMISSION_SESSION_KEY] = permission_url_list

View Code

3、模板中显示菜单信息(session) 将权限初始化函数里的菜单进行循环

rbac/templatetags/rabc.py:(知识点:inclusion_tag用法(自动生成菜单))

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.template import Library
from django.conf import settings

register = Library()

# 找到模板rbac/menu.html将动态数据渲染给模板
@register.inclusion_tag('rbac/menu.html')
def menu(request):
    """生成菜单"""

    menu_list = request.session.get(settings.MENU_SESSION_KEY)
    for item in menu_list:
        regex = "^%s$" % (item['url'],)
        # class=active 标签属性中默认选中状态
        # 如果当前url和菜单列表中的url匹配 那就选中此菜单
        if re.match(regex, request.path_info):
            item['class'] = 'active'

    return {
        'menu_list': request.session.get(settings.MENU_SESSION_KEY)
    }

View Code

将菜单session配置到settings文件中:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

# ######################### 权限相关配置 ############################

PERMISSION_SESSION_KEY = "luffy_permission_url_list_key"
VALID_URL_LIST = [
    '/login/',
    '/admin/.*'
]
MENU_SESSION_KEY = 'menu_session_key'

View Code

模板rbac/templatesrbac/menu.html:(inclusion_tag需要的模板)

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

<div class="static-menu">
    {% for item in menu_list %}
        <a href="{{ item.url }}" class="{{ item.class }}">
            <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span>
            {{ item.title }}
        </a>
    {% endfor %}
</div>

View Code

最终的模板web/templates/layout.html:

导入:{% load rbac %} 

应用:

<div class="menu-body">
         {% menu request %}
</div>

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

{% load staticfiles %}
{% load rbac %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>路飞学城</title>
    <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} ">
    <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/>
    <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/>
    <link rel="stylesheet" href="{% static 'css/commons.css' %} "/>
    <link rel="stylesheet" href="{% static 'css/nav.css' %} "/>
    <style>
        body {
            margin: 0;
        }

        .no-radius {
            border-radius: 0;
        }

        .no-margin {
            margin: 0;
        }

        .pg-body > .left-menu {
            background-color: #EAEDF1;
            position: absolute;
            left: 1px;
            top: 48px;
            bottom: 0;
            width: 220px;
            border: 1px solid #EAEDF1;
            overflow: auto;
        }

        .pg-body > .right-body {
            position: absolute;
            left: 225px;
            right: 0;
            top: 48px;
            bottom: 0;
            overflow: scroll;
            border: 1px solid #ddd;
            border-top: 0;
            font-size: 13px;
            min-width: 755px;
        }

        .navbar-right {
            float: right !important;
            margin-right: -15px;
        }

        .luffy-container {
            padding: 15px;
        }

        .left-menu .menu-body .static-menu {

        }

        .left-menu .menu-body .static-menu .icon-wrap {
            width: 20px;
            display: inline-block;
            text-align: center;
        }

        .left-menu .menu-body .static-menu a {
            text-decoration: none;
            padding: 8px 15px;
            border-bottom: 1px solid #ccc;
            color: #333;
            display: block;
            background: #efefef;
            background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
            background: -ms-linear-gradient(bottom, #efefef, #fafafa);
            background: -moz-linear-gradient(center bottom, #efefef 0%, #fafafa 100%);
            background: -o-linear-gradient(bottom, #efefef, #fafafa);
            filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
            -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
            box-shadow: inset 0px 1px 1px white;
        }

        .left-menu .menu-body .static-menu a:hover {
            color: #2F72AB;
            border-left: 2px solid #2F72AB;
        }

        .left-menu .menu-body .static-menu a.active {
            color: #2F72AB;
            border-left: 2px solid #2F72AB;
        }
    </style>
</head>
<body>

<div class="pg-header">
    <div class="nav">
        <div class="logo-area left">
            <a href="#">
                <img class="logo" src="{% static 'imgs/logo.svg' %}">
                <span style="font-size: 18px;">路飞学城 </span>
            </a>
        </div>

        <div class="left-menu left">
            <a class="menu-item">资产管理</a>
            <a class="menu-item">用户信息</a>
            <a class="menu-item">路飞管理</a>
            <div class="menu-item">
                <span>使用说明</span>
                <i class="fa fa-caret-down" aria-hidden="true"></i>
                <div class="more-info">
                    <a href="#" class="more-item">管他什么菜单</a>
                    <a href="#" class="more-item">实在是编不了</a>
                </div>
            </div>
        </div>

        <div class="right-menu right clearfix">

            <div class="user-info right">
                <a href="#" class="avatar">
                    <img class="img-circle" src="{% static 'imgs/default.png' %}">
                </a>

                <div class="more-info">
                    <a href="#" class="more-item">个人信息</a>
                    <a href="/logout/" class="more-item">注销</a>
                </div>
            </div>

            <a class="user-menu right">
                消息
                <i class="fa fa-commenting-o" aria-hidden="true"></i>
                <span class="badge bg-success">2</span>
            </a>

            <a class="user-menu right">
                通知
                <i class="fa fa-envelope-o" aria-hidden="true"></i>
                <span class="badge bg-success">2</span>
            </a>

            <a class="user-menu right">
                任务
                <i class="fa fa-bell-o" aria-hidden="true"></i>
                <span class="badge bg-danger">4</span>
            </a>
        </div>

    </div>
</div>
<div class="pg-body">
    <div class="left-menu">
        <div class="menu-body">
            {% menu request %}
        </div>
    </div>
    <div class="right-body">
        <div>
            <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">

                <li><a href="#">首页</a></li>
                <li class="active">xx管理</li>

            </ol>
        </div>
        {% block content %} {% endblock %}
    </div>
</div>


<script src="{% static 'js/jquery-3.3.1.min.js' %} "></script>
<script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script>
{% block js %} {% endblock %}
</body>
</html>

View Code

 

二级菜单的编写:

二级菜单 循环两次 渲染 一级菜单里嵌套二级菜单(两层循环)
确定数据结构 倒推功能 数据从数据库而来

二级菜单 思路:

a.  session中存储的菜单数据结构(children二级菜单)

b. 数据库表结构(一级菜单不必有url 不跳转)
(修改 新加一个菜单表 表示一级菜单)

c.页面显示菜单(inclusion_tag循环显示)

 

开发二级菜单:

1、修改表(新加一张菜单表展示一级菜单和二级菜单(权限表绑定)一级菜单无需url不需要跳转):

rbac/model.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

from django.db import models


class Menu(models.Model):
    """
    一级菜单表
    """
    title = models.CharField(verbose_name='菜单', max_length=32)
    icon = models.CharField(verbose_name='图标', max_length=32)

    def __str__(self):
        return self.title


class Permission(models.Model):
    """
    权限表和二级菜单表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示不是菜单;非null表示是二级菜单',
                             on_delete=models.CASCADE)

    def __str__(self):
        return self.title


class Role(models.Model):
    """
    角色
    """
    title = models.CharField(verbose_name='角色名称', max_length=32)
    permissions = models.ManyToManyField(verbose_name='拥有的所有权限', to='Permission', blank=True)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = 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)

    def __str__(self):
        return self.name

View Code

2、获取权限和菜单信息并保存到session中(初始化权限信息)

数据结构的变化字典里套字典再套个列表

{
  1:{
        title:'信息管理'
        iocn:'x1'
        children:[
                { 'title':'个人资料',‘url':'/userinfo/list/'}
        ]
        },

rbac/service/init_permission.py:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django.conf import settings


def init_permission(user, request):
    """
    用户权限初始化
    :param user:
    :param request:
    :return:
    """

    # 根据角色获取所有权限和菜单及图标
    permission_queryset = user.roles.filter(permissions__id__isnull=False).values('permissions__id',
                                                                                  'permissions__title',
                                                                                  'permissions__url',
                                                                                  'permissions__menu_id',
                                                                                  'permissions__menu__title',
                                                                                  'permissions__menu__icon'
                                                                                  ).distinct()

    '''
    {
        1:{
            title:'信息管理',
            iocn:'x1',
            class:"active",
            children:[
                    { 'title':'个人资料',‘url':'/userinfo/list/'},
            ]
            },
    }
    '''

    # 3. 获取权限+菜单信息
    permission_list = []

    menu_dict = {}

    for item in permission_queryset:
        permission_list.append(item['permissions__url'])

        menu_id = item['permissions__menu_id']
        # 如果menu_id为空,就不是二级菜单
        if not menu_id:
            continue
        node = {'title': item['permissions__title'], 'url': item['permissions__url']}
        # 如果有menu_id 将二级菜单按node个格式添加到字典中
        if menu_id in menu_dict:
            menu_dict[menu_id]['children'].append(node)
        else:
            menu_dict[menu_id] = {
                'title': item['permissions__menu__title'],
                'icon': item['permissions__menu__icon'],
                'children': [node, ]
            }
    # 保存到session中
    request.session[settings.PERMISSION_SESSION_KEY] = permission_list
    request.session[settings.MENU_SESSION_KEY] = menu_dict

View Code

3、模板中显示菜单信息(session) 将权限初始化函数里的菜单进行循环(双循环)
知识点 :inclusion_tag

rbac/templatetags/rabc.py:(知识点:inclusion_tag用法(自动生成菜单))

菜单的默认 展示 和 隐藏 给标签加样式就行   class = active  和  hide 样式

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.template import Library
from django.conf import settings
from collections import OrderedDict

register = Library()


# 找到模板rbac/menu.html将动态数据渲染给模板
@register.inclusion_tag('rbac/multi_menu.html')
def multi_menu(request):
    """
    创建二级菜单
    :return:
    """
    menu_dict = request.session[settings.MENU_SESSION_KEY]

    # 对字典的key进行排序
    # 因为字典是无序的,这样菜单展示出来也会是无序的,所以需要排序
    key_list = sorted(menu_dict)

    # 空的有序字典
    ordered_dict = OrderedDict()

    for key in key_list:
        val = menu_dict[key]
        # class=active 标签属性中默认选中状态 hide是隐藏此状态
        # 默认二级菜单隐藏
        val['class'] = 'hide'

        for per in val['children']:
            # 正则匹配要有始^有终$
            regex = "^%s$" % (per['url'],)
            # 如果当前url和菜单列表中的url匹配 那就选中此菜单
            # 将class属性性 改为active
            if re.match(regex, request.path_info):
                per['class'] = 'active'
                val['class'] = ''
        ordered_dict[key] = val

    return {'menu_dict': ordered_dict}

View Code

模板rbac/templatesrbac/multi_menu.html:(inclusion_tag需要的模板)

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

<div class="multi-menu">
    {% for item in menu_dict.values %}
        <div class="item">
            <div class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</div>
            <div class="body {{ item.class }}">
                {% for per in item.children %}
                    <a class="{{ per.class }}" href="{{ per.url }}">{{ per.title }}</a>
                {% endfor %}
            </div>

        </div>
    {% endfor %}
</div>

View Code

给模板添加js和css

rbac/static/rbac/js/rbac.js:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

(function (jq) {
    jq('.multi-menu .title').click(function () {
        $(this).next().toggleClass('hide');
    });
})(jQuery);

View Code

rbac/static/rbac/js/rbac.css:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

.left-menu .menu-body .static-menu {

}

.left-menu .menu-body .static-menu .icon-wrap {
    width: 20px;
    display: inline-block;
    text-align: center;
}

.left-menu .menu-body .static-menu a {
    text-decoration: none;
    padding: 8px 15px;
    border-bottom: 1px solid #ccc;
    color: #333;
    display: block;
    background: #efefef;
    background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
    background: -ms-linear-gradient(bottom, #efefef, #fafafa);
    background: -moz-linear-gradient(center bottom, #efefef 0%, #fafafa 100%);
    background: -o-linear-gradient(bottom, #efefef, #fafafa);
    filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
    box-shadow: inset 0px 1px 1px white;
}

.left-menu .menu-body .static-menu a:hover {
    color: #2F72AB;
    border-left: 2px solid #2F72AB;
}

.left-menu .menu-body .static-menu a.active {
    color: #2F72AB;
    border-left: 2px solid #2F72AB;
}

.multi-menu .item {
    background-color: white;
}

.multi-menu .item > .title {
    padding: 10px 5px;
    border-bottom: 1px solid #dddddd;
    cursor: pointer;
    color: #333;
    display: block;
    background: #efefef;
    background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
    background: -ms-linear-gradient(bottom, #efefef, #fafafa);
    background: -o-linear-gradient(bottom, #efefef, #fafafa);
    filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
    box-shadow: inset 0 1px 1px white;
}

.multi-menu .item > .body {
    border-bottom: 1px solid #dddddd;
}

.multi-menu .item > .body a {
    display: block;
    padding: 5px 20px;
    text-decoration: none;
    border-left: 2px solid transparent;
    font-size: 13px;

}

.multi-menu .item > .body a:hover {
    border-left: 2px solid #2F72AB;
}

.multi-menu .item > .body a.active {
    border-left: 2px solid #2F72AB;
}

View Code

最终要展示的模板web/templates/layout.html:

crm_resource切换资源组 crm组件_crm_resource切换资源组_12

crm_resource切换资源组 crm组件_中间件_13

{% load static %}
{% load rbac %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>路飞学城</title>
    <link rel="shortcut icon" href="{% static 'imgs/luffy-study-logo.png' %} ">
    <link rel="stylesheet" href="{% static 'plugins/bootstrap/css/bootstrap.css' %} "/>
    <link rel="stylesheet" href="{% static 'plugins/font-awesome/css/font-awesome.css' %} "/>
    <link rel="stylesheet" href="{% static 'css/commons.css' %} "/>
    <link rel="stylesheet" href="{% static 'css/nav.css' %} "/>
    <link rel="stylesheet" href="{% static 'rbac/css/rbac.css' %} "/>
    <style>
        body {
            margin: 0;
        }

        .no-radius {
            border-radius: 0;
        }

        .no-margin {
            margin: 0;
        }

        .pg-body > .left-menu {
            background-color: #EAEDF1;
            position: absolute;
            left: 1px;
            top: 48px;
            bottom: 0;
            width: 220px;
            border: 1px solid #EAEDF1;
            overflow: auto;
        }

        .pg-body > .right-body {
            position: absolute;
            left: 225px;
            right: 0;
            top: 48px;
            bottom: 0;
            overflow: scroll;
            border: 1px solid #ddd;
            border-top: 0;
            font-size: 13px;
            min-width: 755px;
        }

        .navbar-right {
            float: right !important;
            margin-right: -15px;
        }

        .luffy-container {
            padding: 15px;
        }

        .left-menu .menu-body .static-menu {

        }

        .left-menu .menu-body .static-menu .icon-wrap {
            width: 20px;
            display: inline-block;
            text-align: center;
        }

        .left-menu .menu-body .static-menu a {
            text-decoration: none;
            padding: 8px 15px;
            border-bottom: 1px solid #ccc;
            color: #333;
            display: block;
            background: #efefef;
            background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #efefef), color-stop(1, #fafafa));
            background: -ms-linear-gradient(bottom, #efefef, #fafafa);
            background: -moz-linear-gradient(center bottom, #efefef 0%, #fafafa 100%);
            background: -o-linear-gradient(bottom, #efefef, #fafafa);
            filter: progid:dximagetransform.microsoft.gradient(startColorStr='#e3e3e3', EndColorStr='#ffffff');
            -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#fafafa',EndColorStr='#efefef')";
            box-shadow: inset 0px 1px 1px white;
        }

        .left-menu .menu-body .static-menu a:hover {
            color: #2F72AB;
            border-left: 2px solid #2F72AB;
        }

        .left-menu .menu-body .static-menu a.active {
            color: #2F72AB;
            border-left: 2px solid #2F72AB;
        }
    </style>
</head>
<body>

<div class="pg-header">
    <div class="nav">
        <div class="logo-area left">
            <a href="#">
                <img class="logo" src="{% static 'imgs/logo.svg' %}">
                <span style="font-size: 18px;">路飞学城 </span>
            </a>
        </div>

        <div class="left-menu left">
            <a class="menu-item">资产管理</a>
            <a class="menu-item">用户信息</a>
            <a class="menu-item">路飞管理</a>
            <div class="menu-item">
                <span>使用说明</span>
                <i class="fa fa-caret-down" aria-hidden="true"></i>
                <div class="more-info">
                    <a href="#" class="more-item">管他什么菜单</a>
                    <a href="#" class="more-item">实在是编不了</a>
                </div>
            </div>
        </div>

        <div class="right-menu right clearfix">

            <div class="user-info right">
                <a href="#" class="avatar">
                    <img class="img-circle" src="{% static 'imgs/default.png' %}">
                </a>

                <div class="more-info">
                    <a href="#" class="more-item">个人信息</a>
                    <a href="#" class="more-item">注销</a>
                </div>
            </div>

            <a class="user-menu right">
                消息
                <i class="fa fa-commenting-o" aria-hidden="true"></i>
                <span class="badge bg-success">2</span>
            </a>

            <a class="user-menu right">
                通知
                <i class="fa fa-envelope-o" aria-hidden="true"></i>
                <span class="badge bg-success">2</span>
            </a>

            <a class="user-menu right">
                任务
                <i class="fa fa-bell-o" aria-hidden="true"></i>
                <span class="badge bg-danger">4</span>
            </a>
        </div>

    </div>
</div>
<div class="pg-body">
    <div class="left-menu">
        <div class="menu-body">
            {% multi_menu request %}

        </div>
    </div>
    <div class="right-body">
        <div>
            <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">

                <li><a href="#">首页</a></li>
                <li class="active">客户管理</li>

            </ol>
        </div>
        {% block content %} {% endblock %}
    </div>
</div>


<script src="{% static 'js/jquery-3.3.1.min.js' %} "></script>
<script src="{% static 'plugins/bootstrap/js/bootstrap.js' %} "></script>
<script src="{% static 'rbac/js/rbac.js' %} "></script>
{% block js %} {% endblock %}
</body>
</html>

View Code