本文将带领零基础开发者从零开始搭建一个完整的客户关系管理系统(CRM),使用Python Django框架实现客户数据的精细化管理。系统包含客户信息管理、联系人跟踪、销售管理、数据可视化等核心功能,全部代码将直接展示并详细解释。
源码及演示:c.xsymz.icu
一、技术栈准备
1.1 环境配置
bash
# 创建虚拟环境
python -m venv crm_env
source crm_env/bin/activate # Linux/Mac
crm_env\Scripts\activate # Windows
# 安装依赖
pip install django django-crispy-forms crispy-bootstrap5 \
django-extensions python-dotenv \
django-import-export pandas matplotlib
1.2 项目初始化
python
# crm/settings.py 基础配置
import os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-*v3q#h$&j6t$')
DEBUG = True
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'crispy_forms',
'crispy_bootstrap5',
'import_export',
'django_extensions',
'crm_app',
]
CRISPY_ALLOWED_TEMPLATE_PACKS = ('bootstrap5',)
CRISPY_TEMPLATE_PACK = 'bootstrap5'
二、数据模型设计
2.1 核心模型定义
python
# crm_app/models.py
from django.db import models
from django.contrib.auth.models import User
class Customer(models.Model):
INDUSTRY_CHOICES = [
('TECH', 'Technology'),
('FINANCE', 'Finance'),
('HEALTH', 'Healthcare'),
('RETAIL', 'Retail'),
]
name = models.CharField('企业名称', max_length=255)
industry = models.CharField('行业', max_length=20, choices=INDUSTRY_CHOICES)
annual_revenue = models.PositiveIntegerField('年收入(万)', blank=True, null=True)
employee_count = models.PositiveIntegerField('员工数量', blank=True, null=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL,
null=True, verbose_name='创建人')
def __str__(self):
return self.name
class Meta:
verbose_name = '客户'
verbose_name_plural = '客户管理'
permissions = [('export_customers', '可以导出客户数据')]
class Contact(models.Model):
customer = models.ForeignKey(Customer, related_name='contacts',
on_delete=models.CASCADE, verbose_name='所属客户')
name = models.CharField('姓名', max_length=100)
job_title = models.CharField('职位', max_length=100, blank=True)
email = models.EmailField('邮箱', max_length=255)
phone = models.CharField('电话', max_length=20, blank=True)
is_primary = models.BooleanField('主要联系人', default=False)
last_interaction = models.DateTimeField('最后联系时间', blank=True, null=True)
class Meta:
verbose_name = '联系人'
verbose_name_plural = '联系人管理'
indexes = [
models.Index(fields=['email']),
models.Index(fields=['customer']),
]
class Opportunity(models.Model):
STAGE_CHOICES = [
('LEAD', '潜在客户'),
('QUALIFIED', '已确认'),
('PROPOSAL', '提案阶段'),
('NEGOTIATION', '谈判阶段'),
('CLOSED_WON', '成交'),
('CLOSED_LOST', '失败'),
]
customer = models.ForeignKey(Customer, on_delete=models.CASCADE,
related_name='opportunities', verbose_name='客户')
name = models.CharField('机会名称', max_length=255)
amount = models.DecimalField('金额', max_digits=12, decimal_places=2)
stage = models.CharField('阶段', max_length=20, choices=STAGE_CHOICES, default='LEAD')
probability = models.PositiveIntegerField('成功率(%)', default=10)
close_date = models.DateField('预计成交日期')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '销售机会'
verbose_name_plural = '销售机会管理'
ordering = ['-created_at']
三、核心功能实现
3.1 客户列表页实现
python
# crm_app/views.py
from django.views.generic import ListView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from .models import Customer
from .forms import CustomerForm
class CustomerListView(LoginRequiredMixin, ListView):
model = Customer
context_object_name = 'customers'
template_name = 'crm_app/customer_list.html'
paginate_by = 25
def get_queryset(self):
queryset = super().get_queryset()
# 添加创建人过滤
if not self.request.user.has_perm('crm_app.export_customers'):
queryset = queryset.filter(created_by=self.request.user)
# 搜索过滤
search_term = self.request.GET.get('search', '')
if search_term:
queryset = queryset.filter(
name__icontains=search_term
)
return queryset.order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_term'] = self.request.GET.get('search', '')
return context
# 创建客户视图
class CustomerCreateView(LoginRequiredMixin, CreateView):
model = Customer
form_class = CustomerForm
template_name = 'crm_app/customer_form.html'
success_url = '/customers/'
def form_valid(self, form):
form.instance.created_by = self.request.user
response = super().form_valid(form)
return response
3.2 联系人管理功能
python
# crm_app/api_views.py
from rest_framework import viewsets
from .models import Contact
from .serializers import ContactSerializer
class ContactViewSet(viewsets.ModelViewSet):
serializer_class = ContactSerializer
def get_queryset(self):
customer_id = self.kwargs.get('customer_id')
if customer_id:
return Contact.objects.filter(customer_id=customer_id)
return Contact.objects.none()
3.3 销售可视化
python
# crm_app/utils/charts.py
import matplotlib.pyplot as plt
from io import BytesIO
import base64
def generate_funnel_chart(opportunities):
stages = ['LEAD', 'QUALIFIED', 'PROPOSAL', 'NEGOTIATION', 'CLOSED_WON']
counts = [opportunities.filter(stage=stage).count() for stage in stages]
plt.figure(figsize=(10,6))
plt.bar(stages, counts, color='#2c3e50')
plt.title('销售漏斗分析')
plt.xlabel('销售阶段')
plt.ylabel('数量')
plt.tight_layout()
buffer = BytesIO()
plt.savefig(buffer, format='png')
plt.close()
return base64.b64encode(buffer.getvalue()).decode('utf-8')
四、前端模板设计
4.1 客户列表模板
django
{# crm_app/templates/crm_app/customer_list.html #}
{% extends 'base.html' %}
{% load crispy_forms_filters %}
{% block content %}
<div class="card shadow mb-4">
<div class="card-header py-3">
<div class="d-flex justify-content-between align-items-center">
<h5 class="m-0 font-weight-bold text-primary">客户管理</h5>
<form method="GET" class="d-flex">
<input type="text" name="search" class="form-control form-control-sm"
placeholder="搜索客户..." value="{{ search_term }}">
<button type="submit" class="btn btn-sm btn-primary">
<i class="fas fa-search"></i>
</button>
</form>
<a href="{% url 'customer-create' %}" class="btn btn-sm btn-success">
<i class="fas fa-plus"></i> 新增客户
</a>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>企业名称</th>
<th>行业</th>
<th>年收入</th>
<th>员工数量</th>
<th>创建人</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for customer in customers %}
<tr>
<td>
<a href="{% url 'customer-detail' customer.pk %}">
{{ customer.name }}
</a>
</td>
<td>{{ customer.get_industry_display }}</td>
<td>¥{{ customer.annual_revenue|default_if_none:"-" }}</td>
<td>{{ customer.employee_count|default_if_none:"-" }}</td>
<td>{{ customer.created_by.username }}</td>
<td>{{ customer.created_at|date:"Y-m-d" }}</td>
<td>
<div class="btn-group">
<a href="{% url 'customer-update' customer.pk %}"
class="btn btn-sm btn-warning">
<i class="fas fa-edit"></i>
</a>
{% if user.has_perm('crm_app.export_customers') %}
<button class="btn btn-sm btn-danger"
onclick="deleteCustomer('{{ customer.pk }}')">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center">没有找到客户记录</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include 'pagination.html' %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function deleteCustomer(id) {
if (confirm('确定要删除这个客户吗?')) {
fetch(`/api/customers/${id}/`, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
window.location.reload();
}
});
}
}
</script>
{% endblock %}
4.2 销售详情页
django
{# crm_app/templates/crm_app/opportunity_detail.html #}
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header">
<h5 class="m-0 font-weight-bold text-primary">
{{ opportunity.name }}
<span class="float-right badge bg-{{ opportunity|stage_color }}">
{{ opportunity.get_stage_display }}
</span>
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>客户信息</h6>
<p>客户名称:{{ opportunity.customer.name }}</p>
<p>行业:{{ opportunity.customer.get_industry_display }}</p>
<p>创建时间:{{ opportunity.created_at|date:"Y-m-d" }}</p>
</div>
<div class="col-md-6">
<h6>机会详情</h6>
<p>金额:¥{{ opportunity.amount }}</p>
<p>成功率:{{ opportunity.probability }}%</p>
<p>预计成交:{{ opportunity.close_date|date:"Y-m-d" }}</p>
</div>
</div>
<h6 class="mt-4">联系人</h6>
<div class="list-group">
{% for contact in opportunity.customer.contacts.all %}
<a href="#" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ contact.name }}</h5>
{% if contact.is_primary %}
<span class="badge bg-primary">主要联系人</span>
{% endif %}
</div>
<p class="mb-1">
<i class="fas fa-envelope"></i> {{ contact.email }} |
<i class="fas fa-phone"></i> {{ contact.phone }}
</p>
<small>{{ contact.job_title }}</small>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header">
<h6 class="m-0 font-weight-bold text-primary">操作面板</h6>
</div>
<div class="card-body">
<button class="btn btn-primary btn-block mb-2"
data-bs-toggle="modal" data-bs-target="#stageModal">
更新阶段
</button>
<a href="{% url 'opportunity-update' opportunity.pk %}"
class="btn btn-warning btn-block mb-2">
编辑机会
</a>
<button class="btn btn-danger btn-block"
onclick="deleteOpportunity('{{ opportunity.pk }}')">
删除机会
</button>
</div>
</div>
<div class="card shadow">
<div class="card-header">
<h6 class="m-0 font-weight-bold text-primary">销售漏斗</h6>
</div>
<div class="card-body">
<img src="data:image/png;base64,{{ chart_data }}"
class="img-fluid" alt="销售漏斗图">
</div>
</div>
</div>
</div>
<!-- 阶段更新模态框 -->
<div class="modal fade" id="stageModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">更新销售阶段</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="stageForm">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">选择新阶段</label>
<select class="form-select" name="stage" required>
{% for key, value in opportunity.STAGE_CHOICES %}
<option value="{{ key }}">{{ value }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">成功率调整</label>
<input type="number" class="form-control"
name="probability" min="0" max="100"
value="{{ opportunity.probability }}">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary"
form="stageForm" onclick="submitStageUpdate('{{ opportunity.pk }}')">
更新
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function submitStageUpdate(opportunityId) {
const formData = new FormData(document.getElementById('stageForm'));
fetch(`/api/opportunities/${opportunityId}/update_stage/`, {
method: 'PATCH',
body: formData
})
.then(response => {
if (response.ok) {
window.location.reload();
}
});
}
function deleteOpportunity(id) {
if (confirm('确定要删除这个销售机会吗?')) {
fetch(`/api/opportunities/${id}/`, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
window.location.href = '/opportunities/';
}
});
}
}
</script>
{% endblock %}
五、数据可视化与分析
5.1 客户行业分布图
python
# crm_app/views.py
from django.views.generic import TemplateView
from .models import Customer
from .utils.charts import generate_pie_chart
class DashboardView(LoginRequiredMixin, TemplateView):
template_name = 'crm_app/dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 客户行业分布
industry_data = Customer.objects.values('industry')\
.annotate(count=models.Count('id'))\
.order_by('-count')
context['industry_chart'] = generate_pie_chart(
industry_data,
'industry',
'count',
'客户行业分布'
)
# 销售漏斗数据
from .utils.charts import generate_funnel_chart
from .models import Opportunity
context['funnel_chart'] = generate_funnel_chart(
Opportunity.objects.all()
)
return context
5.2 数据导出功能
python
# crm_app/admin.py
from import_export.admin import ImportExportModelAdmin
from django.contrib import admin
from .models import Customer
@admin.register(Customer)
class CustomerAdmin(ImportExportModelAdmin):
list_display = ('name', 'industry', 'annual_revenue', 'created_by', 'created_at')
search_fields = ('name', 'industry')
list_filter = ('industry', 'created_by')
resource_class = CustomerResource
python
# crm_app/resources.py
from import_export import resources, fields
from .models import Customer
class CustomerResource(resources.ModelResource):
class Meta:
model = Customer
fields = ('id', 'name', 'industry', 'annual_revenue',
'employee_count', 'created_by__username', 'created_at')
export_order = fields
六、系统安全与权限控制
6.1 用户认证与授权
python
# crm_app/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.core.mail import send_mail
@receiver(post_save, sender=User)
def send_welcome_email(sender, instance, created, **kwargs):
if created:
subject = '欢迎使用CRM系统'
message = f'您好 {instance.username},您的账户已创建,请开始管理您的客户数据!'
send_mail(
subject,
message,
'noreply@crm-system.com',
[instance.email],
fail_silently=False,
)
6.2 权限策略配置
python
# crm_app/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.created_by == request.user
结语
通过本指南,零基础开发者已经完成了从零搭建CRM系统的全流程。系统包含完整的客户管理、联系人跟踪、销售机会管理、数据可视化等核心功能,并实现了用户认证、权限控制、数据导出等企业级功能。