零基础玩转 CRM 源码:快速搭建客户关系管理系统,实现客户数据精细化管理

77 阅读5分钟

本文将带领零基础开发者从零开始搭建一个完整的客户关系管理系统(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

1.jpeg

结语

通过本指南,零基础开发者已经完成了从零搭建CRM系统的全流程。系统包含完整的客户管理、联系人跟踪、销售机会管理、数据可视化等核心功能,并实现了用户认证、权限控制、数据导出等企业级功能。