写在前面

之前在windows上写代码逻辑、搞前端等花了很长时间,跑通之后一直没往centos上部署,

昨天尝试部署下,结果发现静态文件找不到 ==''

由于写了2个组件:

  - arya  model的增删改查,模拟django admin 

  - rbac  基于角色的访问控制

并且每个组件下都有自己的静态文件,层次结构如下:

[root@standby crm_rbac_arya]# tree -I "statics|*pyc|migrations" . -L 3
.
├── arya
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── models.py
│   ├── __pycache__
│   ├── service
│   │   ├── arya.py
│   │   ├── arya_v1.py
│   │   └── __pycache__
│   ├── static
│   │   └── arya
│   ├── templates
│   │   └── arya
│   ├── tests.py
│   ├── utils
│   │   ├── pager.py
│   │   └── __pycache__
│   └── views.py
├── bin
│   ├── uwsgi.ini
│   ├── uwsgi.log
│   ├── uwsgi.pid
│   └── uwsgi.sock
├── crm
│   ├── admin.py
│   ├── apps.py
│   ├── arya.py
│   ├── __init__.py
│   ├── middleware
│   │   ├── login_required.py
│   │   └── __pycache__
│   ├── models.py
│   ├── __pycache__
│   ├── tests.py
│   └── views.py
├── crm_rbac_arya
│   ├── __init__.py
│   ├── __pycache__
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── manage.py
├── rbac
│   ├── admin.py
│   ├── apps.py
│   ├── arya.py
│   ├── __init__.py
│   ├── middleware
│   │   ├── __pycache__
│   │   └── rbac.py
│   ├── models.py
│   ├── __pycache__
│   ├── service
│   │   ├── init_permission.py
│   │   └── __pycache__
│   ├── static
│   │   └── rbac
│   ├── templates
│   │   └── rbac
│   ├── templatetags
│   │   ├── __init__.py
│   │   ├── menu_gennerator.py
│   │   └── __pycache__
│   ├── tests.py
│   └── views.py
└── templates
├── arya
│   ├── layout.html.simple
│   └── layout_old.html
├── index.html
└── login.html 31 directories, 42 files
[root@standby crm_rbac_arya]#

  

开始纠结:

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'rbac/static')
STATIC_ROOT = os.path.join(BASE_DIR, 'arya/static')  

之前这样写的,没有写 STATICFILES_DIRS , 并且在urls.py里增加了如下几行:

from django.conf import settings
from django.conf.urls.static import static urlpatterns = [
url(r'^arya/', arya.site.urls),
url(r'^login/', views.login),
url(r'^index/', views.index),
url(r'^clear/', views.clear),
] urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

这样是可以找到arya的静态文件,找不到rbac的静态文件。

后来想把多个app下的静态文件都移出来放一个目录下,但是又不想破坏每个组件的完整性。。。

看了官网Managing static files 苦逼了好一会,瞎搞了一会还是没搞定。

今早在地铁上,又上网查了下,突然灵机一动想起了 STATICFILES_DIRS ,必须有 django.contrib.staticfiles 这个app,然后

python manage.py collectstatic

最后在nginx和uwsgi上配置好路径即可!

环境:

Python 3.5.2

django 1.11.4

CentOS release 6.4 (Final)

nginx/1.10.3

  

废话到此为止,上代码:

arya/service/arya.py

from django.shortcuts import HttpResponse,render,redirect
from django.conf.urls import url, include
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.forms import ModelForm
from ..utils.pager import Paginator
from copy import deepcopy
from django.db.models import ForeignKey, ManyToManyField
import functools
from types import FunctionType
from django.db.models import Q
from django.http.request import QueryDict class FilterRow(object):
"""
组合搜索项
"""
def __init__(self, option, change_list, data_list, param_dict=None, is_choices=None):
self.option = option
self.change_list = change_list
self.data_list = data_list
self.param_dict = deepcopy(param_dict)
self.param_dict._mutable = True
self.is_choices = is_choices def __iter__(self):
base_url = self.change_list.config.reverse_list_url
tpl = "<a href='{0}' class='{1}'>{2}</a>"
"""
点击 课程2 和 性别1 这两个条件进行筛选的情况下:
self.option.name 分别是 consultant course gender
self.param_dict 是 <QueryDict: {'gender': ['1'], 'course': ['2']}>
""" # 这里是给 全部btn 创建url链接
if self.option.name in self.param_dict:
# 注意这里需要先把option.name对应的item pop掉,再做 urlencode()操作!
pop_value = self.param_dict.pop(self.option.name)
url = "{0}?{1}".format(base_url, self.param_dict.urlencode())
val = tpl.format(url, '', '全部')
self.param_dict.setlist(self.option.name, pop_value)
else:
url = "{0}?{1}".format(base_url, self.param_dict.urlencode())
val = tpl.format(url, 'active', '全部') # self.param_dict
yield mark_safe("<div class='whole'>")
yield mark_safe(val)
yield mark_safe("</div>") yield mark_safe("<div class='others'>")
for obj in self.data_list:
param_dict = deepcopy(self.param_dict)
if self.is_choices:
# ((1, '男'), (2, '女'))
pk = str(obj[0])
text = obj[1]
else:
# url上要传递的值
pk = self.option.val_func_name(obj) if self.option.val_func_name else obj.pk
pk = str(pk)
# a标签上显示的内容
text = self.option.text_func_name(obj) if self.option.text_func_name else str(obj) exist = False
if pk in param_dict.getlist(self.option.name):
exist = True if self.option.is_multi:
if exist:
values = param_dict.getlist(self.option.name)
values.remove(pk)
param_dict.setlist(self.option.name,values)
else:
param_dict.appendlist(self.option.name, pk)
else:
param_dict[self.option.name] = pk
url = "{0}?{1}".format(base_url, param_dict.urlencode())
val = tpl.format(url, 'active' if exist else '', text)
yield mark_safe(val)
yield mark_safe("</div>") class FilterOption(object):
def __init__(self, field_or_func, condition=None, is_multi=False, text_func_name=None, val_func_name=None):
"""
:param field: 字段名称或函数
:param is_multi: 是否支持多选
:param text_func_name: 在Model中定义函数,显示文本名称,默认使用 str(对象)
:param val_func_name: 在Model中定义函数,显示文本名称,默认使用 对象.pk
"""
self.field_or_func = field_or_func
self.condition = condition # 筛选条件
self.is_multi = is_multi # 是否允许多选
self.text_func_name = text_func_name
self.val_func_name = val_func_name @property
def is_func(self):
if isinstance(self.field_or_func, FunctionType):
return True @property
def name(self):
if self.is_func:
return self.field_or_func.__name__
else:
return self.field_or_func @property
def get_condition(self):
if self.condition:
return self.condition
con = Q()
return con class ChangeList(object):
"""
专门用来处理列表页面部分的代码逻辑,简化 AryaConfig.changelist_view()
"""
def __init__(self,config,queryset):
self.config = config
self.list_display = config.get_list_display()
self.show_add = config.get_show_add()
self.add_url = config.reverse_add_url
# 模糊搜索
self.search_list = config.get_search_list()
self.keyword = config.keyword
self.actions = config.get_actions() # 分页相关
current_page = config.request.GET.get('page',1)
all_count = queryset.count()
base_url = config.reverse_list_url
per_page = config.per_page
per_page_count = config.per_page_count # 用于首先模糊查找了下数据的情况下要保留原来的 ?keyword=xxx ,在这基础上再进行分页
# 但是如果在这里修改query_params则会影响 request.GET ,所以这里要进行深拷贝
# 注意:request.GET 不是字典类型,而是django自己的QueryDict类型
query_params = deepcopy(config.request.GET)
query_params._mutable = True pager = Paginator(all_count,current_page,base_url,per_page,per_page_count,query_params)
self.queryset = queryset[pager.start:pager.end]
self.page_html = pager.page_html # 组合筛选
self.list_filter = config.get_list_filter() # 获取表头第一版
'''
header_data = []
for str_or_func in self.get_list_display():
if isinstance(str_or_func,str):
val = self.model._meta.get_field(str_or_func).verbose_name
else:
val = str_or_func(self, is_header=True)
header_data.append(val)
'''
# 获取表头改进版
def table_header(self):
for str_or_func in self.list_display:
if isinstance(str_or_func, str):
val = self.config.model._meta.get_field(str_or_func).verbose_name
else:
val = str_or_func(self.config, is_header=True)
yield val # 获取表内容
# def table_body(self):
# table_data = []
# for row in self.queryset:
# if not self.list_display:
# # 用列表把对象做成列表集合是为了兼容有list_display的情况在前端展示(前端用2层循环展示)
# table_data.append([row, ])
# else:
# tmp = []
# for str_or_func in self.list_display:
# if isinstance(str_or_func, str):
# # 如果是字符串则通过反射取值
# tmp.append(getattr(row, str_or_func))
# else:
# # 否则就是函数,获取函数执行的结果
# tmp.append(str_or_func(self.config, row))
# table_data.append(tmp)
# return table_data
def table_body(self):
for row in self.queryset:
if not self.list_display:
# 用列表把对象做成列表集合是为了兼容有list_display的情况在前端展示(前端用2层循环展示)
yield [row, ]
else:
tmp = []
for str_or_func in self.list_display:
if isinstance(str_or_func, str):
# 如果是字符串则通过反射取值
tmp.append(getattr(row, str_or_func))
else:
# 否则就是函数,获取函数执行的结果
tmp.append(str_or_func(self.config, row))
yield tmp # 定制批量操作的actions
def action_options(self):
options = []
for func in self.actions:
tmp = {'value':func.__name__, 'text':func.text}
options.append(tmp)
return options # 定制组合筛选
def gen_list_filter(self):
for option in self.list_filter:
if option.is_func:
data_list = option.field_or_func(self.config, self, option)
else:
_field = self.config.model._meta.get_field(option.field_or_func)
"""
option.field_or_func course 咨询的课程
_field crm.Customer.course type <class 'django.db.models.fields.related.ManyToManyField'>
_field.rel <ManyToManyRel: crm.customer> type <class 'django.db.models.fields.reverse_related.ManyToManyRel'> option.field_or_func consultant 课程顾问
_field crm.Customer.consultant type <class 'django.db.models.fields.related.ForeignKey'>
_field.rel <ManyToOneRel: crm.customer> type <class 'django.db.models.fields.reverse_related.ManyToOneRel'>
"""
if isinstance(_field, ForeignKey):
data_list = FilterRow(option, self, _field.rel.model.objects.filter(option.get_condition),
self.config.request.GET)
elif isinstance(_field, ManyToManyField):
data_list = FilterRow(option, self, _field.rel.model.objects.filter(option.get_condition),
self.config.request.GET)
else:
# print(_field.choices) # ((1, '男'), (2, '女'))
data_list = FilterRow(option, self, _field.choices, self.config.request.GET, is_choices=True)
yield data_list def add_html(self):
"""
添加按钮
:return:
"""
add_html = mark_safe('<a class="btn btn-primary" href="%s">添加</a>' % (self.config.add_url_params,))
return add_html def search_attr(self):
val = self.config.request.GET.get(self.keyword)
return {"value": val, 'name': self.keyword} class AryaConfig(object): # 借助继承特性,实现定制列展示
list_display = [] # 定制是否显示添加按钮
show_add = False
def get_show_add(self):
return self.show_add # 使用ModelForm
model_form_class = None
def get_model_form_class(self):
if self.model_form_class:
return self.model_form_class
class DynamicModelForm(ModelForm):
class Meta:
model = self.model
fields = '__all__'
return DynamicModelForm
"""
也可以使用 type 来生成
def get_model_form_class(self):
model_form_cls = self.model_form
if not model_form_cls:
_meta = type('Meta', (object,), {'model': self.model, "fields": "__all__"})
model_form_cls = type('DynamicModelForm', (ModelForm,), {'Meta': _meta})
return model_form_cls
""" # 分页相关配置
per_page = 10
per_page_count = 11 # 定制actions,即结合checkbox进行批量操作
actions = []
def get_actions(self):
result = []
result.extend(self.actions)
return result # 模糊搜索字段列表 (默认不支持搜索)
search_list = []
def get_search_list(self):
result = []
result.extend(self.search_list)
return result @property
def get_search_condition(self):
con = Q()
con.connector = "OR"
# 加入搜索关键字是 kk, 并且如果我们在search_list里规定的只有 qq 和 name 这俩字段可以提供搜索条件
# 那么 kk 这个关键字要么在 name里,要么在qq这个字段里,二者之间是 或 的关系
val = self.request.GET.get(self.keyword)
if not val:
return con
# ['qq','name'] 精确搜索
# ['qq__contains','name__contains'] 模糊搜索
field_list = self.get_search_list()
for field in field_list:
field = "{0}__contains".format(field)
con.children.append((field,val))
return con @property
def get_search_condition2(self):
'''
search_list = [
{'key': 'qq', 'type': None},
{'key': 'name', 'type': None},
{'key': 'course__name', 'type': None},
]
'''
# condition = {}
# keyword = request.GET.get('keyword')
# search_list = self.get_search_list()
# if keyword and search_list:
# # ['username','email','ut',]
# for field in search_list:
# condition[field] = keyword
# condition = {
# 'username':keyword,
# 'email':keyword,
# 'ut':keyword,
# }
# 这样去 filter(**condition) 过滤的时候是按照 且 关系过滤, 这样不太好,应该改成 或 关系过滤
# 即 Django里的 Q 查询 : from django.db.models import Q
# queryset = self.model.objects.all()
# queryset = self.model.objects.filter(**condition)
# 增加这个属性,用于在ChangeList类里获取到查询的关键字(即通过self参数把request传递给ChangeList)
condition = Q()
condition.connector = "OR"
keyword = self.request.GET.get(self.keyword)
if not keyword:
return condition
search_list = self.get_search_list()
for field_dict in search_list:
field = "{0}__contains".format(field_dict.get('key'))
field_type = field_dict.get('type')
if field_type:
try:
keyword = field_type(keyword)
except Exception as e:
continue
condition.children.append((field, keyword))
return condition """定制查询组合条件"""
list_filter = []
def get_list_filter(self):
return self.list_filter @property
def get_list_filter_condition(self):
# 获取model的字段,FK,choice,但是没有多对多的字段
# fields1 = [obj.name for obj in self.model._meta.fields]
# 只获取获取多对多的字段
# fields2 = [obj.name for obj in self.model._meta.many_to_many]
# 还包含了反向关联字段
fields3 = [obj.name for obj in self.model._meta._get_fields()]
"""
['internal_referral', 'consultrecord', 'paymentrecord', 'student', 'id', 'qq', \
'name', 'gender', 'education', 'graduation_school', 'major', 'experience', 'work_status', \
'company', 'salary', 'source', 'referral_from', 'status', 'consultant', 'date', 'last_consult_date', 'course']
"""
# fields = dir(self.model._meta)
"""
['FORWARD_PROPERTIES', 'REVERSE_PROPERTIES', '__class__', '__delattr__', '__dict__', '__dir__', \
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', \
'__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', \
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_expire_cache', '_forward_fields_map', \
'_get_fields', '_get_fields_cache', '_ordering_clash', '_populate_directed_relation_graph', '_prepare', \
'_property_names', '_relation_tree', 'abstract', 'add_field', 'add_manager', 'app_config', 'app_label', 'apps', \
'auto_created', 'auto_field', 'base_manager', 'base_manager_name', 'can_migrate', 'concrete_fields', 'concrete_model', \
'contribute_to_class', 'db_table', 'db_tablespace', 'default_apps', 'default_manager', 'default_manager_name', \
'default_permissions', 'default_related_name', 'fields', 'fields_map', 'get_ancestor_link', 'get_base_chain', \
'get_field', 'get_fields', 'get_latest_by', 'get_parent_list', 'get_path_from_parent', 'get_path_to_parent', \
'has_auto_field', 'index_together', 'indexes', 'installed', 'label', 'label_lower', 'local_concrete_fields', \
'local_fields', 'local_managers', 'local_many_to_many', 'managed', 'manager_inheritance_from_future', 'managers', \
'managers_map', 'many_to_many', 'model', 'model_name', 'object_name', 'order_with_respect_to', 'ordering', \
'original_attrs', 'parents', 'permissions', 'pk', 'private_fields', 'proxy', 'proxy_for_model', 'related_fkey_lookups', \
'related_objects', 'required_db_features', 'required_db_vendor', 'select_on_save', 'setup_pk', 'setup_proxy', \
'swappable', 'swapped', 'unique_together', 'verbose_name', 'verbose_name_plural', 'verbose_name_raw', 'virtual_fields']
""" # 去请求URL中获取参数
# 根据参数生成条件
con = {}
params = self.request.GET
# self.request.GET <QueryDict: {'gender': ['1'], 'course': ['1', '2']}>
for k in params:
# 判断k是否在数据库字段支持
if k not in fields3:
continue
v = params.getlist(k)
k = "{0}__in".format(k)
con[k] = v
"""
比如按照课程2和性别1这俩条件进行筛选的时候:
{'gender__in': ['1'], 'course__in': ['2']}
并且课程可以多选 注意:这里课程之间是 或 的关系,即如果一个客户只咨询了课程1,但是筛选条件是 课程1和课程2,这种情况下,当前客户也会被筛选出来,
尽管该用户并没有咨询课程2 <QueryDict: {'gender': ['2'], 'course': ['1', '2']}>
{'course__in': ['1', '2'], 'gender__in': ['2']}
"""
return con def __init__(self, model, arya_site):
self.model = model
self.arya_site = arya_site
self.app_label = model._meta.app_label
self.model_name = model._meta.model_name
self.change_filter_name = "_change_filter"
self.keyword = 'keyword'
self.request = None # 定制 编辑 按钮
def row_edit(self, row=None, is_header=None):
if is_header:
return "编辑"
# 反向生成URL
edit_a = mark_safe("<a href='{0}?{1}'>编辑</a>".format(self.reverse_edit_url(row.id), self.back_url_param))
return edit_a # 定制 删除 按钮
def row_del(self, row=None, is_header=None):
if is_header:
return "删除"
# 反向生成URL
del_a = mark_safe("<a href='{0}?{1}'>删除</a>".format(self.reverse_del_url(row.id), self.back_url_param))
return del_a # 定制 checkbox
def check_box(self, row=None, is_header=None):
if is_header:
return "选项"
checkbox = mark_safe("<input type='checkbox' name='item_id' value='{0}' />".format(row.id))
return checkbox def get_list_display(self):
result = []
result.extend(self.list_display)
# 如果有编辑权限
"""
注意这里的参数不是方法self.row_edit 而是函数AryaConfig.row_edit
class Foo(object):
def func(self):
print('方法') 方法和函数的区别:
# - 如果被对象调用,则self不用传值
# obj = Foo()
# obj.func() # - 如果被类 调用,则self需要主动传值
# obj = Foo()
# Foo.func(obj)
"""
result.append(AryaConfig.row_edit)
# 如果有删除权限
result.append(AryaConfig.row_del)
# 加上checkbox
result.insert(0, AryaConfig.check_box)
return result # 装饰器:给 changelist_view add_view delete_view change_view 增加 self.request = request
# 这样就不用在每个view里都写一遍 self.request = request
# 每次请求进来记录下这个request,这样就能拿到rbac请求验证中间里面的permission_code_list
def wrapper(self, func):
@functools.wraps(func)
def inner(request, *args, **kwargs):
self.request = request
return func(request, *args, **kwargs)
return inner def get_urls(self):
app_model_name = self.model._meta.app_label,self.model._meta.model_name
urlpatterns = [
url(r'^$', self.wrapper(self.changelist_view), name='%s_%s_list' % app_model_name),
url(r'^add/$', self.wrapper(self.add_view), name='%s_%s_add' % app_model_name),
url(r'^(.+)/delete/$', self.wrapper(self.delete_view), name='%s_%s_delete' % app_model_name),
url(r'^(.+)/change/$', self.wrapper(self.change_view), name='%s_%s_change' % app_model_name)
]
urlpatterns += self.extra_urls()
return urlpatterns def extra_urls(self):
"""
扩展URL预留的钩子函数
:return:
"""
return [] @property
def urls(self):
return self.get_urls(), None, None def changelist_view(self, request):
"""
列表页面
:param request:
:return:
"""
# 执行批量actions,比如批量删除
if 'POST' == request.method:
func_name = request.POST.get('select_action')
if func_name:
# 通过反射获取要批量执行的函数对象
func = getattr(self, func_name)
func(request) '''先过滤组合搜索,然后过滤模糊搜索,最后去重拿到最后结果'''
queryset = self.model.objects.filter(**self.get_list_filter_condition).filter(self.get_search_condition2).distinct()
cl = ChangeList(self,queryset)
return render(request,'arya/item_list.html',{'cl':cl}) def add_view(self, request):
"""
添加页面
:param request:
:return:
"""
model_form_cls = self.get_model_form_class()
if 'GET' == request.method:
# 返回对应的添加页面
form = model_form_cls()
return render(request,'arya/add_view.html',{'form':form})
else:
# 保存
form = model_form_cls(data=request.POST)
if form.is_valid():
form.save()
# 获取反向生成URL,跳转回列表页面
return redirect(self.list_url_with_params)
return render(request,'arya/add_view.html',{'form':form}) def delete_view(self, request, uid):
"""
删除页面
:param request:
:param uid:
:return:
"""
obj = self.model.objects.filter(id=uid).first()
if not obj:
return redirect(self.reverse_list_url)
if 'GET' == request.method:
return render(request,'arya/delete_view.html')
else:
obj.delete()
return redirect(self.list_url_with_params) def change_view(self, request, uid):
"""
编辑页面
:param request:
:param uid:
:return:
"""
obj = self.model.objects.filter(id=uid).first()
if not obj:
return redirect(self.reverse_list_url)
model_form_cls = self.get_model_form_class()
if 'GET' == request.method:
# 在input框里显示原来的值
form = model_form_cls(instance=obj)
return render(request,'arya/change_view.html',{'form':form})
else:
# 更新某个实例
form = model_form_cls(instance=obj,data=request.POST)
if form.is_valid():
form.save()
return redirect(self.list_url_with_params)
return render(request, 'arya/change_view.html', {'form': form}) # 反向生成url相关
@property
def back_url_param(self):
'''反向生成base_url之外的其他参数,用于保留之前的操作'''
query = QueryDict(mutable=True)
if self.request.GET:
"""
self.request.GET <QueryDict: {'gender': ['1'], 'course': ['1', '2']}>
self.request.GET.urlencode() gender=1&course=1&course=2
query.urlencode() _change_filter=gender%3D1%26course%3D1%26course%3D2 对应的编辑按钮的地址: /arya/crm/customer/obj.id/change/?_change_filter=gender%3D1%26course%3D1%26course%3D2
"""
query[self.change_filter_name] = self.request.GET.urlencode() # gender=2&course=2&course=1
return query.urlencode() def reverse_del_url(self, pk):
'''反向生成删除按钮对应的基础URL(不带额外参数的),需要传入obj的id'''
base_del_url = reverse(viewname='{0}:{1}_{2}_delete'.format(self.arya_site.namespace, self.app_label, self.model_name),args=(pk,))
return base_del_url def reverse_edit_url(self, pk):
'''反向生成编辑按钮对应的基础URL(不带额外参数的),需要传入obj的id'''
base_edit_url = reverse(viewname='{0}:{1}_{2}_change'.format(self.arya_site.namespace, self.app_label, self.model_name),args=(pk,))
return base_edit_url @property
def reverse_add_url(self):
'''反向生成添加按钮对应的基础URL(不带额外参数的)'''
base_add_url = reverse(viewname='{0}:{1}_{2}_add'.format(self.arya_site.namespace, self.app_label, self.model_name))
return base_add_url @property
def reverse_list_url(self):
'''反向生成列表页面对应的基础URL(不带额外参数的)'''
base_list_url = reverse(viewname='{0}:{1}_{2}_list'.format(self.arya_site.namespace, self.app_label, self.model_name))
return base_list_url @property
def list_url_with_params(self):
'''反向生成列表页面对应的URL(带了之前用户操作的一些参数)'''
base_url = self.reverse_list_url
query = self.request.GET.get(self.change_filter_name)
return "{0}?{1}".format(base_url, query if query else "") @property
def add_url_params(self):
base_url = self.reverse_add_url
if self.request.GET:
return base_url
else:
query = QueryDict(mutable=True)
query[self.change_filter_name] = self.request.GET.urlencode()
return "{0}?{1}".format(base_url, query.urlencode()) class AryaSite(object):
def __init__(self, name='arya'):
self.name = name
self.namespace = name
self._registy = {} def register(self,class_name,config_class):
self._registy[class_name] = config_class(class_name,self) def get_urls(self):
urlpatterns = [
url(r'^login/$', self.login),
url(r'^logout/$', self.logout),
]
for model, config_class in self._registy.items():
pattern = r'^{0}/{1}/'.format(model._meta.app_label, model._meta.model_name)
urlpatterns.append(url(pattern, config_class.urls))
# return urlpatterns,None,None
# 指定名称空间名字为 arya
return urlpatterns @property
def urls(self):
return self.get_urls(),self.name,self.namespace def login(self, request):
return HttpResponse("登录页面")
def logout(self, request):
return HttpResponse("登出页面") # 基于Python文件导入特性实现的单例模式
site = AryaSite()

  

arya/apps.py

from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules
from django.contrib.admin.sites import site class AryaConfig(AppConfig):
name = 'arya' def ready(self):
autodiscover_modules('arya', register_to=site)

  

crm/models.py里的顾客model

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')
date = models.DateField(verbose_name="咨询日期", auto_now_add=True)
last_consult_date = models.DateField(verbose_name="最后跟进日期", auto_now_add=True) def __str__(self):
return "姓名:{0},QQ:{1}".format(self.name, self.qq, )

  

crm/arya.py里顾客部分

from arya.service import arya
from . import models
from django.forms import ModelForm,fields
from django.forms import widgets as form_widgets
from django.utils.safestring import mark_safe
from django.shortcuts import HttpResponse,render,redirect
from django.db.models import Q class CustomerModelForm(ModelForm): # 也可以自己在这里添加一个字段
# phone = fields.CharField()
# city = fields.ChoiceField(choices=[(1,"北京"),(2,"上海"),(3,"深圳")])
# 注意:这里扩展的字段名如果和 models.Customer 里面的字段名相同就会覆盖 models.Customer的字段,否则则会添加一个新的字段 class Meta:
model = models.Customer
fields = '__all__'
error_messages = {
'qq':{
'required':'qq不能为空!',
},
'name': {
'required': '客户姓名不能为空!',
},
'gender': {
'required': '性别不能为空!',
},
'source': {
'required': '客户来源不能为空!',
},
'course': {
'required': '咨询的课程不能为空!',
},
'status': {
'required': '客户状态不能为空!',
},
'consultant':{
'required': '课程顾问不能为空!',
}
} class CustomerConfig(PermissionConfig, arya.AryaConfig):
def show_gender(self, row=None, is_header=None):
if is_header:
return "性别"
# gender_choices = ((1, '男'), (2, '女'))
# gender = models.SmallIntegerField(verbose_name='性别', choices=gender_choices)
# obj.get_字段_display() 这个方法可以拿到 数字在元组里对应的描述
return row.get_gender_display()
def show_education(self, row=None, is_header=None):
if is_header:
return "学历"
# obj.get_字段_display() 这个方法可以拿到 数字在元组里对应的描述
return row.get_education_display()
def show_work_status(self, row=None, is_header=None):
if is_header:
return "职业状态"
# obj.get_字段_display() 这个方法可以拿到 数字在元组里对应的描述
return row.get_work_status_display()
def show_experience(self, row=None, is_header=None):
if is_header:
return "工作经验"
# obj.get_字段_display() 这个方法可以拿到 数字在元组里对应的描述
return row.get_experience_display()
def show_course(self, row=None, is_header=None):
if is_header:
return "咨询的课程"
tpl = "<span style='display:inline-block;padding:3px;margin:2px;border:1px solid #ddd;'>{0}</span>"
course_obj_list = row.course.all()
courses = [tpl.format(course.name) for course in course_obj_list]
return mark_safe(' '.join(courses)) def show_record(self, row=None, is_header=None):
if is_header:
return "跟进记录"
return mark_safe("<a href='xxx/{0}'>查看跟进记录</a>".format(row.id)) list_display = ['qq','name',show_gender,show_course,'consultant',show_record] model_form_class = CustomerModelForm # 定制批量删除的actions
def multi_delete(self, request):
item_list = request.POST.getlist('item_id')
# 注意:filter(id__in=item_list) 这样写就不用使用for循环了
self.model.objects.filter(id__in=item_list).delete() multi_delete.text = "批量删除" # 可以这样赋值
actions = [multi_delete,] # search_list = [
# {'key': 'qq__contains', 'type': None},
# {'key': 'name__contains', 'type': None},
# {'key': 'course__name__contains', 'type': None},
# ]
search_list = [
{'key': 'qq', 'type': None},
{'key': 'name', 'type': None},
{'key': 'course__name', 'type': None},
] list_filter = [
arya.FilterOption('consultant', condition=Q(depart_id=1)),
arya.FilterOption('course', is_multi=True),
arya.FilterOption('gender'),
] arya.site.register(models.Customer, CustomerConfig)

  

rbac/arya.py里权限部分

from arya.service import arya
from . import models
from django.forms import ModelForm,fields,widgets
from django.urls.resolvers import RegexURLPattern
from crm.arya import PermissionConfig as PermissionControl # 获取全部url
def get_all_url(patterns,prev,is_first=False, result=[]):
if is_first:
result.clear()
for item in patterns:
v = item._regex.strip("^$")
if isinstance(item, RegexURLPattern):
val = prev + v
result.append((val,val,))
# result.append(val)
else:
get_all_url(item.urlconf_name, prev + v)
return result class PermissionModelForm(ModelForm):
# 也可以自己在这里添加扩展字段
# phone = fields.CharField()
# city = fields.ChoiceField(choices=[(1,"北京"),(2,"上海"),(3,"深圳")])
# city = fields.MultipleChoiceField(choices=[(1,"北京"),(2,"上海"),(3,"深圳")])
# 注意:这里扩展的字段名如果和 models.Customer 里面的字段名相同就会覆盖 models.Customer的字段,否则则会添加一个新的字段
url = fields.ChoiceField() class Meta:
model = models.Permission
fields = '__all__'
# fields = ['title','url']
# exclude = ['title']
error_messages = {
'title':{
'required':'用户名不能为空!',
},
'url': {
'required': '密码不能为空!',
},
'code': {
'required': '密码不能为空!',
},
'group': {
'invalid': '邮箱格式不正确!',
},
}
# 也可以自定义前端标签样式
# widgets = {
# 'username': form_widgets.Textarea(attrs={'class': 'c1'})
# 'username': form_widgets.Input(attrs={'class': 'some_class'})
# }
def __init__(self, *args, **kwargs):
super(PermissionModelForm,self).__init__(*args, **kwargs)
from crm_rbac_arya.urls import urlpatterns
# 获取全部url,并以下拉框的形式显示在前端
# 也可以进一步把未加入权限的url列出来,就需要查一遍数据库过滤下。
self.fields['url'].choices = get_all_url(urlpatterns, '/', True) """
# 在用Form的时候遇到过这个问题,即用户关联部门(外键关联)的时候:
# depart = fields.ChoiceField(choices=models.Department.objects.values_list('id','title'))
# 如果按照上面方式写,那么如果在部门表新添加数据后,则在用户关联的时候是无法显示新添加的部门信息的!!!只有程序重启才能获得新添加的数据!
# 因为 depart 在 UserInfoForm 类里属于静态字段,在程序刚启动的时候会从上到下执行一遍,把当前数据加载到内存。
# 所以采用了 __init__() 方法,每次都去数据库拿最新的数据
手动挡:
depart = fields.ChoiceField()
def __init__(self, *args, **kwargs):
super(UserInfoForm,self).__init__(*args, **kwargs)
self.fields['depart'].choices = models.Department.objects.values_list('id','title')
自动挡:
from django.forms.models import ModelChoiceField
depart = ModelChoiceField(queryset=models.Department.objects.all())
# 这种方式虽然简单,但是在前端<option value=pk>object</option>,即显示的是object,还依赖model里的 __str__方法。 上面说的是Form的问题,而ModelForm是Form和Model的结合体,也存在这个问题,所以这里也采用 __init__() 的方式
""" class PermissionConfig(PermissionControl, arya.AryaConfig):
list_display = ['title','url','group',]
# 定制添加权限页面
model_form_class = PermissionModelForm arya.site.register(models.Permission, PermissionConfig)

  

rbac/middleware/rbac.py权限验证中间件

# 这是页面权限验证的中间件
from django.shortcuts import HttpResponse,redirect
from django.conf import settings
import re class MiddlewareMixin(object):
def __init__(self, get_response=None):
self.get_response = get_response
super(MiddlewareMixin, self).__init__() def __call__(self, request):
response = None
if hasattr(self, 'process_request'):
response = self.process_request(request)
if not response:
response = self.get_response(request)
if hasattr(self, 'process_response'):
response = self.process_response(request, response)
return response class RbacMiddleware(MiddlewareMixin):
def process_request(self,request): # 1. 获取当前请求的 uri
current_request_url = request.path_info # 2. 判断是否在白名单里,在则不进行验证,直接放行
for url in settings.VALID_URL_LIST:
if re.match(url, current_request_url):
return None # 3. 验证用户是否有访问权限
flag = False
permission_dict = request.session.get(settings.PERMISSION_DICT) # 如果没有登录过就直接跳转到登录页面
if not permission_dict:
return redirect(settings.RBAC_LOGIN_URL)
"""
{
1: {
'codes': ['list', 'add'],
'urls': ['/userinfo/', '/userinfo/add/']
},
2: {
'codes': ['list'],
'urls': ['/order/']
}
}
"""
for group_id, values in permission_dict.items():
for url in values['urls']:
# 必须精确匹配 URL : "^{0}$"
patten = settings.URL_FORMAT.format(url)
if re.match(patten, current_request_url):
# 获取当前用户所具有的权限的代号列表,用于之后控制是否展示相关操作
request.permission_code_list = values['codes']
flag = True
break
if flag:
break
if not flag:
return HttpResponse("无权访问")

  

settings.py

"""
Django settings for crm_rbac_arya project. Generated by 'django-admin startproject' using Django 1.11.4. For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
""" import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '6-s)^llfgdh3jl-d682cb55ef2a@&&k7po_7rvqi%c8%=#&4(f' # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rbac.apps.RbacConfig',
'arya.apps.AryaConfig',
'crm.apps.CrmConfig',
] 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',
'crm.middleware.login_required.UserAuthMiddleware',
'rbac.middleware.rbac.RbacMiddleware',
] ROOT_URLCONF = 'crm_rbac_arya.urls' TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
] WSGI_APPLICATION = 'crm_rbac_arya.wsgi.application' # Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
} # Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
] # Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/ STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'statics')
#STATIC_ROOT = os.path.join(BASE_DIR, 'rbac/static')
#STATIC_ROOT = os.path.join(BASE_DIR, 'arya/static')
#STATICFILES_DIRS = (
# os.path.join(BASE_DIR,"common_static"),
# '/data/www/crm_rbac_arya/arya/static/',
#) ########################## Private config ################################## PERMISSION_DICT = "permission_dict"
PERMISSION_MENU_LIST = "permission_menu_list"
URL_FORMAT = "^{0}$"
RBAC_LOGIN_URL = "/login/"
LOGIN_SESSION_KEY = "user_info"
VALID_URL_LIST = [
"^/login/$",
"^/admin.*",
"^/clear/$",
"^/static/*",
]

  

主模板

{% load static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>新起点</title>
<link rel="Shortcut Icon" href="{% static 'arya/img/header.png' %}"/>
<link rel="stylesheet" href="{% static 'arya/plugin/layui/css/layui.css' %}">
{% block css %} {% endblock %}
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
<div class="layui-header">
<div class="layui-logo">在线教育CRM</div>
<!-- 头部区域(可配合layui已有的水平导航) -->
<ul class="layui-nav layui-layout-left">
<li class="layui-nav-item"><a href="">虚拟化</a></li>
<li class="layui-nav-item"><a href="">大数据</a></li>
<li class="layui-nav-item"><a href="">图像识别</a></li>
<li class="layui-nav-item">
<a href="javascript:;">其它方向</a>
<dl class="layui-nav-child">
<dd><a href="">邮件管理</a></dd>
<dd><a href="">消息管理</a></dd>
<dd><a href="">授权管理</a></dd>
</dl>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item">
<a href="javascript:;">
<img src="{% static 'arya/img/avatar.jpg' %}" class="layui-nav-img">
standby
</a>
<dl class="layui-nav-child">
<dd><a href="">基本资料</a></dd>
<dd><a href="">安全设置</a></dd>
</dl>
</li>
<li class="layui-nav-item"><a href="/clear/">退出</a></li>
</ul>
</div> {% load menu_gennerator %}
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<!-- 左侧导航区域(可配合layui已有的垂直导航) -->
<div class="left_menu">
{% menu_show request %}
</div>
</div>
</div> <div class="layui-body">
<!-- 内容主体区域 -->
<div style="padding: 15px;">
{% block content %} {% endblock %}
</div>
</div> <div class="layui-footer" style="text-align: center;">
<!-- 底部固定区域 -->
Copyright@<a href="http://www.cnblogs.com/standby/" target="_blank">71standby</a>
</div>
</div>
<script src="{% static 'arya/plugin/jquery/js/jquery-3.2.1.js' %}"></script>
<script src="{% static 'arya/plugin/layui/layui.all.js' %}"></script>
<script src="{% static 'rbac/js/rbac_layui.js' %}"></script> {% block js %} {% endblock %} <script>
;!function () {
//无需再执行layui.use()方法加载模块,直接使用即可
var form = layui.form
, layer = layui.layer; //…
}();
</script>
</body>
</html>

  

列表页面模板

{% extends "arya/layout.html" %}
{% load static %} {% block css %}
<link rel="stylesheet" href="{% static 'arya/plugin/bootstrap/css/bootstrap.css' %}">
<link rel="stylesheet" href="{% static 'arya/css/filter.css' %}">
<link rel="stylesheet" href="{% static 'arya/css/option.css' %}">
{% endblock %} {% block content %} <div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/index/">首页</a>
<a href="" class="breadcrumb_menu_title"></a>
<a href="" class="breadcrumb_menu_item"><cite></cite></a>
</span>
</div> <div> <!-- 组合筛选 -->
{% if cl.list_filter %}
<div class="comb-search">
{% for row in cl.gen_list_filter %}
<div class="row">
{% for col in row %}
{{ col }}
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %} <!-- 模糊搜索 -->
{% if cl.search_list %}
<div class="search_option">
<form action="" method="get">
<input class="form-control" id="key_input" name="{{ cl.search_attr.name }}" value="{{ cl.search_attr.value }}" type="text" placeholder="请输入关键字..." />
<button class="btn btn-success">
<span class="glyphicon glyphicon-search"></span>
</button>
</form>
</div>
{% endif %} <!-- 模糊搜索方式2 -->
{# <div class="search_option">#}
{# {% if cl.search_list %}#}
{# <form method="get">#}
{# <input type="text" name="keyword" id="key_input" class="form-control" placeholder="请输入搜索关键字..." value="{{ cl.keyword }}">#}
{# <input type="submit" value="搜索" class="btn btn-primary">#}
{# </form>#}
{# {% endif %}#}
{# </div>#} <!-- 添加button -->
{# {% if cl.show_add %}#}
{# {{ cl.add_html }}#}
{# {% endif %}#} <!-- 定制Action和表格数据 -->
<form method="post">
{% csrf_token %} {% if cl.actions %}
<div class="multi_option">
<select name="select_action" class="form-control" style="width: 300px; display: inline-block">
{% for action in cl.action_options %}
<option value="{{ action.value }}">{{ action.text }}</option>
{% endfor %}
</select>
<input type="submit" value="执行" class="btn btn-success">
</div>
{% endif %} <table class="table table-striped table-hover">
<thead>
<tr>
{% for val in cl.table_header %}
<th>{{ val }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for item in cl.table_body %}
<tr>
{% for col in item %}
<td>{{ col }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</form> <div style="text-align: right">
<ul class="pagination">
{{ cl.page_html|safe }}
</ul>
</div> </div>
{% endblock %} {% block js %}
<script src="{% static 'arya/plugin/bootstrap/js/bootstrap.js' %}"></script>
<script src="{% static 'arya/js/breadcrumb.js' %}"></script>
{% endblock %}

uwsgi.ini

# uwsig使用配置文件启动
[uwsgi]
# 项目目录
chdir=/data/www/crm_rbac_arya/
# 指定项目的application
module=crm_rbac_arya.wsgi:application
# 指定sock的文件路径
socket=/data/www/crm_rbac_arya/bin/uwsgi.sock
# 进程个数
workers=6
pidfile=/data/www/crm_rbac_arya/bin/uwsgi.pid
# 指定IP端口
http=ip:port
# 指定静态文件
static-map=/static=/data/www/crm_rbac_arya/statics
# 启动uwsgi的用户名和用户组
uid=root
gid=root
# 启用主进程
master=true
# 自动移除unix Socket和pid文件当服务停止的时候
vacuum=true
# 序列化接受的内容,如果可能的话
thunder-lock=true
# 启用线程
enable-threads=true
# 设置自中断时间
harakiri=30
# 设置缓冲
post-buffering=4096
# 设置日志目录
daemonize=/data/www/crm_rbac_arya/bin/uwsgi.log

  

crm.conf

server {
listen 80;
access_log logs/crm.log main;
root /data/www/crm_rbac_arya; location /static {
alias /data/www/crm_rbac_arya/statics;
} location / {
include uwsgi_params;
# uwsgi_pass 127.0.0.1:80;
uwsgi_pass unix:/data/www/crm_rbac_arya/bin/uwsgi.sock;
} }

  

成果截图:

并且针对修改和删除操作,使用QueryDict(mutable=True)对象实例记录操作前的参数,保留了之前的操作步骤。

扩展

QueryDict的mutable参数 :

更多请参考官方文档:Django的Request 对象和Response 对象

遗留的bug

如果先按照关键字搜索,
然后翻页,
然后再做组合筛选的话,由于page参数停留在翻页之后所以会导致组合筛选的时候可能会搜索不到。

 

项目源码已托管至 Github

自定义CRM系统的更多相关文章

  1. 适合企业的CRM系统选型法则?

    在市场竞争激烈的今天,企业需要找到一款好用的企业CRM系统来帮助维护客户关系,同时也能够帮助企业进行销售管理.营销管理,CRM可以说是当代企业管理的最强工具之一.那么适合企业的CRM客户管理系统要如何 ...

  2. 如何在CRM系统中集成ActiveReports最终报表设计器

    有时候,将ActiveReports设计器集成到业务系统中,为用户提供一些自定义的数据表,用户不需要了解如何底层的逻辑关系和后台代码,只需要选择几张关联的数据表,我们会根据用户的选择生成可供用户直接使 ...

  3. CRM系统之stark组件流程分析

    CRM系统主要通过自定义stark组件来实现的(参照admin系统自定义): STARK组件: 1 admin组件 1 如何使用admin 2 admin源码 3 创建自己的admin组件:stark ...

  4. 大量客户名片如何轻松导入到CRM系统里?

    当您组织或参与了一次线下活动或展会,肯定会收集到非常多的潜在客户的名片.这个时候您是不是在发愁如何将这些信息导入到CRM系统中? 可以想到,您肯定会将这些名片分发给销售人员,让他们手动录入--这也确实 ...

  5. CRM系统个性化定制的对企业的优势作用

    伴随着科学技术的不断发展,企业信息化建设也在持续地开展.企业管理模式已经开始由传统模式向信息化转变,并且越来越多的企业开始使用互联网软件来进行辅助管理,这一趋势也让CRM客户管理系统得到快速的发展.市 ...

  6. CRM系统选型时的参考哪些方面

    企业不论在制定营销策略或是在进行CRM系统选型时,首先都是要了解自身的需求.每一家企业的情况和需求都有很大差异,CRM系统的功能也都各有偏重.有些CRM偏重销售管理.有些注重于营销自动化.有些则侧重于 ...

  7. 探究国内CRM系统哪家公司做的最好?

    国内CRM系统哪家公司做的最好?相信这是很多人关心的话题.但这是一个伪命题,因为无论什么产品,都没有一个确定的结论来证明哪个产品最好.我们只能根据它的功能.适用性.价格等来判断哪个最合适.所以小编只能 ...

  8. CRM系统如何帮助企业管理多条业务线的?

    在如今的市场环境中,许多企业为了提高销售效率,增加业绩收入,都会选择使用CRM客户关系管理系统来帮助进行对客户和销售的管理.CRM系统能够帮助企业在不同的产品线上同时开展营销活动.各个销售团队能够独立 ...

  9. CRM系统简析

    寄语: 简单阐述一下对CRM系统应用的理解,此内容参考网上资料所整理. CRM是Customer Relationship Management的缩写,简称客户关系管理. CRM系统可以从三个方面来分 ...

随机推荐

  1. PKUWC2019 凉凉记

    请配合 BGM 食用. 菜就是菜,说什么都是借口. Day 0 前一天先到纪中报道,高铁上打了一会单机膈膜,然后又打了一遍 \(FFT\) 板子,就到了中山. 到了后,发现气温骤然升高,马上 脱 换裤 ...

  2. ubuntu配置mysql

    1.安装mysql: sudo apt-get install mysql-server sudo apt-get install mysql-client sudo apt-get install ...

  3. [TJOI2012]桥(最短路+线段树)

    有n个岛屿, m座桥,每座桥连通两座岛屿,桥上会有一些敌人,玩家只有消灭了桥上的敌人才能通过,与此同时桥上的敌人会对玩家造成一定伤害.而且会有一个大Boss镇守一座桥,以玩家目前的能力,是不可能通过的 ...

  4. 使用python简单连接并操作数据库

    python中连接并操作数据库 图示操作流程 一.使用的完整流程 # 1. 导入模块 from pymysql import connect # 2. 创建和数据库服务器的连接,自行设置 服务器地址, ...

  5. A1141. PAT Ranking of Institutions

    After each PAT, the PAT Center will announce the ranking of institutions based on their students' pe ...

  6. linux free命令

    Linux上的free命令详解 free命令的所有输出值都是从/proc/meminfo中读出的 total used free shared buffers cached Mem: -/+ buff ...

  7. long long

    1. ll a; scanf("%d",&a); 数据读入后,产生错误 2. const ll inf=1e18; 3. int * ll = ll ll * int = ...

  8. snpeff注释变异(variants)

    1.进入网站http://snpeff.sourceforge.net/,下载snpeff: wget http://sourceforge.net/projects/snpeff/files/snp ...

  9. Java实现二叉树的前序、中序、后序、层序遍历(非递归方法)

      在上一篇博客中,实现了Java中二叉树的四种遍历方式的递归实现,接下来,在此实现Java中非递归实现二叉树的前序.中序.后序.层序遍历,在非递归实现中,借助了栈来帮助实现遍历.前序和中序比较类似, ...

  10. post请求中data参数的应用

    一.data为参数,json是自动的把参数转换成了json格式,一般建议用json ,url是请求地址. 二,以一个网站来做解释,看登陆的请求 抓包看一下: 用在代码里面看一下: 如果不转的话,那么用 ...