目录
一、系统概述
客户关系管理系统(CRM)是利用信息科学技术,实现市场营销、销售、服务等活动自动化,是企业能更高效地为客户提供满意、周到的服务,以提高客户满意度、忠诚度为目的的一种管理经营方式。客户关系管理既是一种管理理念,又是一种软件技术。以客户为中心的管理理念是CRM实施的基础。
业务模块
- 客户管理:增删改查,客户名称,公司地址,联系方式等信息
- 产品管理:增删改查,树状结构,自定义多产品类别,多产品名称
- 商机管理:增删改查,辅助业务员对客户持续跟进,挖掘商机,制定推进计划
- 合同管理:增删改查,包含交货方式,付款方式,合同状态等信息
- 客户公海:增删改查,管理员制定策略自动将久未更新的客户划入公海,或手动划入公海
- 数据导入导出功能:业务数据模块均支持
- 数据报表:按时间(日月年)进行数据统计分析
- 企业驾驶舱:将销售关键指标提炼出来,以数据可视化方式展示给企业决策者,助力更有价值的决策经营
- 部门管理:增删改查,树状结构,与企业组织架构一一对应
- 用户管理:增删改查,与企业员工一一对应
- 角色管理:管理员,部分负责人,业务员等权限
- 消息中心:群发通知,可以指定部门,指定用户
- 日志管理:所有模块的增删改查数据,便于问题追踪回溯
部署方式
支持Windows、Linux、Mac等各种操作系统;
观看方式:既可在服务器上直接观看程序界面,也可在远程用浏览器打开播放,例如Chrome浏览器、360浏览器等。
二、效果展示
登录页面
客户页面
联系人
产品类目
产品
商机
合同
消息中心
供应商
客户公海
三、关键代码
后端 http 使用 python django
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
后端数据库模型代码 - 客户管理
class Customer(CoreModel):
name = models.CharField(
max_length=150, unique=True, db_index=True, verbose_name="客户名称", help_text="客户名称")
mobile = models.CharField(
max_length=255, verbose_name="手机", null=True, blank=True, help_text="手机")
telephone = models.CharField(
max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话")
email = models.EmailField(
max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱")
address = models.CharField(
max_length=255, verbose_name="客户公司地址", null=True, blank=True, help_text="客户公司地址")
task_status = models.IntegerField(
default=0, verbose_name="跟进状态", help_text="跟进状态")
task_record = models.CharField(
max_length=255, verbose_name="最新跟进记录", null=True, blank=True, help_text="最新跟进记录")
is_active = models.BooleanField(
default=0, verbose_name="启用 or 停用", help_text="启用 or 停用")
owner = models.ForeignKey(
to="Users",
related_name='customer_owner',
verbose_name="负责人",
on_delete=models.PROTECT,
db_constraint=False,
null=True,
blank=True,
help_text="负责人",
)
dept = models.ForeignKey(
to="Dept",
verbose_name="所属部门",
on_delete=models.PROTECT,
db_constraint=False,
null=True,
blank=True,
help_text="关联部门",
)
# contacter = models.ForeignKey(
# to="Contacter",
# related_name='contacter_name',
# verbose_name="联系人",
# on_delete=models.PROTECT,
# db_constraint=False,
# null=True,
# blank=True,
# help_text="联系人",
# )
class Meta:
db_table = table_prefix + "customer"
verbose_name = "客户表"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)
后端业务模块代码 - 客户管理
from django_restql.fields import DynamicSerializerMethodField
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from application import dispatch
from dvadmin.system.models import Customer, Dept, Contacter
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.system.views.contacter import ContacterSerializer
class CustomerSerializer(CustomModelSerializer):
"""
客户管理-序列化器
"""
dept_name = serializers.CharField(source="dept.name", default="")
owner_name = serializers.CharField(source="users.name", default="")
contacter_name = serializers.CharField(source="contacter.name", default="")
class Meta:
model = Customer
fields = "__all__"
read_only_fields = ["id"]
extra_kwargs = {
}
class CustomerInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
class Meta:
model = Customer
fields = "__all__"
read_only_fields = ['id']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class CustomerCreateSerializer(CustomModelSerializer):
"""
客户新增-序列化器
"""
name = serializers.CharField(
max_length=50,
validators=[
CustomUniqueValidator(
queryset=Customer.objects.all(), message="客户名称必须唯一")
],
)
def save(self, **kwargs):
data = super().save(**kwargs)
data.save()
return data
class Meta:
model = Customer
fields = "__all__"
read_only_fields = ["id"]
extra_kwargs = {
"post": {"required": False},
}
class CustomerUpdateSerializer(CustomModelSerializer):
"""
客户修改-序列化器
"""
name = serializers.CharField(
max_length=50,
validators=[
CustomUniqueValidator(
queryset=Customer.objects.all(), message="账号必须唯一")
],
)
# password = serializers.CharField(required=False, allow_blank=True)
mobile = serializers.CharField(
max_length=50,
validators=[
CustomUniqueValidator(
queryset=Customer.objects.all(), message="手机号必须唯一")
],
allow_blank=True
)
def save(self, **kwargs):
data = super().save(**kwargs)
data.save()
return data
class Meta:
model = Customer
read_only_fields = ["id"]
fields = "__all__"
extra_kwargs = {
"post": {"required": False, "read_only": True},
}
class ExportCustomerSerializer(CustomModelSerializer):
"""
客户导出 序列化器
"""
is_active = serializers.SerializerMethodField(read_only=True)
dept_name = serializers.CharField(source="dept.name", default="")
# dept_owner = serializers.CharField(source="dept.owner", default="")
def get_is_active(self, instance):
return "启用" if instance.is_active else "停用"
class Meta:
model = Customer
fields = (
"name",
"email",
"mobile",
"is_active",
)
class CustomerImportSerializer(CustomModelSerializer):
def save(self, **kwargs):
data = super().save(**kwargs)
data.save()
return data
class Meta:
model = Customer
exclude = (
"date_joined",
)
class CustomerViewSet(CustomModelViewSet):
"""
客户接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = Customer.objects.exclude().all()
serializer_class = CustomerSerializer
create_serializer_class = CustomerCreateSerializer
update_serializer_class = CustomerUpdateSerializer
filter_fields = {
"name": ["icontains"],
"mobile": ["icontains"],
"telephone": ["icontains"],
"owner": ["exact"],
# "contacter": ["exact"],
"task_status": ["exact"],
"is_active": ["exact"],
}
# search_fields = ["name", "contacter",
# "status", "mobile", "owner"]
# 导出
export_field_label = [
"客户名称",
"客户邮箱",
"手机号码",
"帐号状态",
]
export_serializer_class = ExportCustomerSerializer
# 导入
import_serializer_class = CustomerImportSerializer
import_field_dict = {
"name": "客户名称",
"email": "客户邮箱",
"mobile": "手机号码",
"is_active": {
"title": "帐号状态",
"choices": {
"data": {"启用": True, "禁用": False},
}
},
}
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def customer_info(self, request):
"""获取当前客户信息"""
customer = request.customer
result = {
"name": customer.name,
"mobile": customer.mobile,
"email": customer.email,
}
return DetailResponse(data=result, msg="获取成功")
@action(methods=["PUT"], detail=False, permission_classes=[IsAuthenticated])
def update_customer_info(self, request):
"""修改当前客户信息"""
customer = request.customer
Customer.objects.filter(id=customer.id).update(**request.data)
return DetailResponse(data=None, msg="修改成功")
def customer_lazy_tree(self, request, *args, **kwargs):
parent = self.request.query_params.get('parent')
queryset = self.filter_queryset(self.get_queryset())
if not parent:
if self.request.user.is_superuser:
queryset = queryset.filter(parent__isnull=True)
else:
queryset = queryset.filter(id=self.request.user.dept_id)
data = queryset.filter(status=True).order_by(
'sort').values('name', 'id', 'parent')
return DetailResponse(data=data, msg="获取成功")
前端页面 vue 代码
<template>
<d2-container :class="{ 'page-compact': crud.pageOptions.compact }">
<d2-crud-x ref="d2Crud" v-bind="_crudProps" v-on="_crudListeners">
<div slot="header">
<crud-search
ref="search"
:options="crud.searchOptions"
@submit="handleSearch"
/>
<el-button-group>
<el-button
size="small"
type="primary"
v-permission="'Create'"
@click="addRow"
>
<i class="el-icon-plus" /> 新增
</el-button>
<el-button size="small" type="danger" @click="batchDelete">
<i class="el-icon-delete"></i> 批量删除
</el-button>
<el-button size="small" type="warning" @click="onExport" v-permission="'Export'"><i class="el-icon-download" /> 导出
</el-button>
<importExcel importApi="api/system/customer/import/" v-permission="'Import'"
>导入
</importExcel>
</el-button-group>
<crud-toolbar
:search.sync="crud.searchOptions.show"
:compact.sync="crud.pageOptions.compact"
:columns="crud.columns"
@refresh="doRefresh()"
@columns-filter-changed="handleColumnsFilterChanged"
/>
</div>
</d2-crud-x>
</d2-container>
</template>
<script>
import * as api from './api'
import { crudOptions } from './crud'
import { d2CrudPlus } from 'd2-crud-plus'
export default {
name: 'customer',
mixins: [d2CrudPlus.crud],
data() {
return {}
},
methods: {
getCrudOptions() {
this.crud.searchOptions.form.user_type = 0
return crudOptions(this)
},
pageRequest(query) {
return api.GetList(query)
},
addRequest(row) {
return api.AddObj(row)
},
updateRequest(row) {
return api.UpdateObj(row)
},
delRequest(row) {
return api.DelObj(row.id)
},
batchDelRequest(ids) {
return api.BatchDel(ids)
},
onExport() {
const that = this
this.$confirm('是否确认导出所有数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function () {
const query = that.getSearch().getForm()
return api.exportData({ ...query })
})
}
}
}
</script>
<style lang="scss">
.yxtInput {
.el-form-item__label {
color: #49a1ff;
}
}
</style>
前端业务逻辑处理
import { urlPrefix as userPrefix } from '../system/user/api'
export const crudOptions = (vm) => {
return {
pageOptions: {
compact: true
},
options: {
height: '100%',
tableType: 'vxe-table',
rowKey: true,
rowId: 'id'
},
selectionRow: {
align: 'center',
width: 46
},
rowHandle: {
width: 240,
fixed: 'right',
view: {
thin: true,
text: '',
disabled() {
return !vm.hasPermissions('Retrieve')
}
},
edit: {
thin: true,
text: '',
disabled() {
return !vm.hasPermissions('Update')
}
},
remove: {
thin: true,
text: '',
disabled() {
return !vm.hasPermissions('Delete')
}
}
},
viewOptions: {
componentType: 'form'
},
formOptions: {
defaultSpan: 12 // 默认的表单 span
},
indexRow: {
// 或者直接传true,不显示title,不居中
title: '序号',
align: 'center',
width: 60
},
columns: [
{
title: 'ID',
key: 'id',
disabled: true,
form: {
disabled: true
}
},
{
title: '客户名称',
key: 'name',
search: {
disabled: false
},
minWidth: 100,
type: 'input',
form: {
rules: [
// 表单校验规则
{
required: true,
message: '客户名称必填项'
}
],
component: {
placeholder: '请输入客户名称'
},
itemProps: {
class: { yxtInput: true }
}
}
},
{
title: '电话',
key: 'telephone',
search: {
disabled: true
},
minWidth: 110,
type: 'input',
form: {
rules: [
{
max: 20,
message: '请输入正确的电话号码',
trigger: 'blur'
},
{
// pattern: /^0\d{11-15}$/,
message: '请输入正确的手机号码'
}
],
component: {
placeholder: '示例 0755-34513212'
}
}
},
{
title: '手机号码',
key: 'mobile',
search: {
disabled: true
},
minWidth: 140,
type: 'input',
form: {
rules: [
{
max: 20,
message: '请输入正确的手机号码',
trigger: 'blur'
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码'
}
],
component: {
placeholder: '请输入手机号码'
}
}
},
{
title: '邮箱',
key: 'email',
minWidth: 180,
form: {
rules: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change']
}
],
component: {
placeholder: '请输入邮箱'
}
}
},
{
title: '公司地址',
key: 'address',
type: 'input',
minWidth: 180,
form: {
rules: [
{
message: '请输入正确的公司地址',
trigger: ['blur', 'change']
}
],
component: {
placeholder: '请输入公司地址'
}
}
},
{
title: '跟进状态',
key: 'task_status',
search: {
disabled: false
},
type: 'select',
width: 145,
dict: {
data: vm.dictionary('customer_task_status')
},
form: {
value: 1,
component: {
span: 12
}
},
component: { props: { color: 'auto' } } // 自动染色
},
{
title: '负责人',
key: 'owner',
minWidth: 90,
search: {
disabled: false
},
type: 'tree-selector',
dict: {
cache: false,
isTree: true,
url: userPrefix,
value: 'id', // 数据字典中value字段的属性名
label: 'username' // 数据字典中label字段的属性名
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '负责人必填项'
}
],
itemProps: {
class: { yxtInput: true }
},
component: {
span: 12,
pagination: true,
props: { multiple: false }
}
}
},
// {
// title: '部门',
// key: 'dept',
// search: {
// disabled: true
// },
// minWidth: 140,
// type: 'tree-selector',
// dict: {
// cache: false,
// isTree: true,
// url: deptPrefix,
// value: 'id', // 数据字典中value字段的属性名
// label: 'name' // 数据字典中label字段的属性名
// },
// form: {
// rules: [
// // 表单校验规则
// {
// required: true,
// message: '必填项'
// }
// ],
// itemProps: {
// class: { yxtInput: true }
// },
// component: {
// span: 12,
// pagination: true,
// props: { multiple: false }
// }
// },
// component: {
// name: 'foreignKey',
// valueBinding: 'dept_name'
// }
// },
{
title: '是否启用',
key: 'is_active',
search: {
disabled: false
},
width: 70,
type: 'radio',
dict: {
data: vm.dictionary('button_status_bool')
},
form: {
value: true,
component: {
span: 12
}
}
}
].concat(
vm.commonEndColumns({
create_datetime: { showTable: false },
update_datetime: { showTable: false }
})
)
}
}
参考资料
本次分享结束,欢迎交流定制 ~
本文含有隐藏内容,请 开通VIP 后查看