☰
Django-CRM源码实战:模型设计、权限改造与生产部署指南
2026/10/28 1:04:15 网站建设 项目流程

简介:一套采用Python高效开发、基于Django框架搭建的客户关系管理(CRM)系统完整源码,适合熟悉Python基础、希望借助成熟Django项目上手CRM业务开发或系统学习Django工程实践的开发者。压缩包共有652个文件,总体积约10.1MB,涉及Python、HTML、JavaScript、CSS、Shell等多种技术;其中204个Python脚本覆盖模型、视图、路由等后端核心逻辑,67个HTML页面、16个JavaScript脚本与13个CSS样式表构成前端展示与交互层,另有202张JPG、84张PNG图片资源,以及Dockerfile、Shell脚本、YAML配置、Markdown文档等部署与辅助文件,目录结构清晰,便于快速浏览和按模块复用。目前已有549人学习/下载,借助这套源码,开发者可以直观理解Django中URL分发、ORM操作、模板渲染、用户认证与权限控制等关键机制的落地方式;同时,内置的静态资源、样式与图片可直接替换使用,大幅减少前端搭建耗时,使二次开发更聚焦于业务逻辑本身。整体是一套兼具学习与实用价值的完整CRM项目参考,无论用于毕业设计、课程项目还是企业内训,都可从中借鉴清晰的分层结构、通用设计模式与高效的Django写法。

1. 拿到Django-CRM源码,先别急着跑:这套系统到底值不值得改

拿到一套Django-CRM的完整源码,大多数人的第一反应是迫不及待地把它跑起来,我劝你先冷静十分钟。这套基于Python和Django框架开发的客户关系管理系统,所谓“完整源码”意味着你拿到的不只是一堆models.py和views.py,而是从客户线索录入、销售跟进记录、合同审批到权限分配的一条完整业务闭环。它适合两类人:一类是中小团队的技术负责人,想用开源项目替代昂贵的SaaS服务,把客户数据稳稳掌握在自己手里;另一类是Django项目实战新手,想找一个有真实业务逻辑的工程来精读,而不是重复图书管理系统demo。反直觉的结论是:耗时的不是启动服务,而是把里面的数据模型改造成你们销售团队真正习惯的工作流。

2. 从数据模型看懂CRM:客户、跟进记录与合同的状态机设计

2.1 四个核心模型的关系与字段设计

一套能长期维护的Django-CRM,数据模型一定不是随便堆几个表。常见做法是围绕四个核心模型展开:Customer(客户)、Contact(联系人)、FollowUp(跟进记录)、Contract(合同)。Customer是业务主体,具备名称、状态、归属销售等基础字段;Contact挂在Customer之下,因为一个客户往往对应多个对接人,而实际跟你谈合同的是联系人而不是那家公司;FollowUp记录每一次电话或拜访,是销售漏斗分析的数据来源;Contract则保存金额、审批状态和签订时间,直接关系业绩统计。

# customers/models.py from django.db import models from django.conf import settings class Customer(models.Model): STATUS_CHOICES = ( ('lead', '线索'), ('potential', '潜在客户'), ('customer', '成交客户'), ('lost', '已流失'), ) name = models.CharField(max_length=100, verbose_name='客户名称') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='lead', db_index=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='customers') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name class Contact(models.Model): customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='contacts') name = models.CharField(max_length=50) phone = models.CharField(max_length=20) position = models.CharField(max_length=50, blank=True) class FollowUp(models.Model): customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='follow_ups') content = models.TextField(verbose_name='跟进内容') next_time = models.DateTimeField(null=True, blank=True, verbose_name='下次跟进时间') created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True) created_at = models.DateTimeField(auto_now_add=True)

这段代码里有三个值得玩味的参数。owner的外键用的是on_delete=models.PROTECT,意思是只要这个销售名下还挂着客户,就不允许直接把他从用户表里删掉,这在CRM里很重要——防止误删业务数据。Contact的CASCADE则相反,客户删了联系人跟着删,因为联系人没有独立业务价值。status字段加了db_index=True,因为销售列表页和公海筛选几乎都会按状态过滤,不加索引数据量一上来就翻车。next_time是跟进提醒的依据,后续做消息通知全靠它。

2.2 客户生命周期用状态字段而不是靠删数据

很多新手改造CRM时最容易犯的错,是把“客户流失”实现成“删除客户记录”。等老板要求统计这个月流失了多少客户时,数据已经没了,只能干瞪眼。正确做法是用状态字段表达生命周期:新建客户是lead,销售跟进后变成potential,签单后是customer,长期不成交则标记为lost。这样一份客户记录从头到尾只有一条,所有历史都能追溯,报表也只需要group by status就能画漏斗图。

# 错误的做法:直接物理删除流失客户 Customer.objects.filter(status='lost').delete() # 数据没了,财报也救不回来 # 正确的做法:把状态改掉,记录还在 Customer.objects.filter(id=123).update(status='lost')

用ORM执行查询和删除对象时要克制,这是血泪经验。delete()是物理删除,调用之后数据不进回收站,Django的QuerySet.delete()也不会触发模型里的delete()方法。我一般会在项目里给重要模型加一个is_active布尔字段,查询默认带filter(is_active=True),删除统一走is_active=False,相当于给自己留一颗后悔药。只有清理测试数据这种场景,才允许直接调delete()。

2.3 自定义User模型与权限分配

企业级Django项目实战新手最容易忽略的第一道坎,是用户模型。Django内置的User只有用户名、邮箱这类基础字段,但CRM需要手机号、部门、上级主管这些销售属性,所以几乎每个CRM项目都会继承AbstractUser重写一套用户模型。关键约束是:必须在第一次migrate之前就设置好AUTH_USER_MODEL,否则后面改会引发连环迁移错误。

# settings.py AUTH_USER_MODEL = 'accounts.User' # accounts/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone = models.CharField(max_length=20, blank=True, verbose_name='手机号') department = models.CharField(max_length=50, blank=True, verbose_name='部门') manager = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='上级主管') class Meta: db_table = 'crm_user'

权限方面,Django自带PermissionMixin和分组机制,能满足“销售只能看自己的客户,主管看全组”这种粒度的需求。做法是在User上加一个role字段,取值如sales、manager、admin,然后用django.contrib.auth.models.Group给不同角色绑定不同权限码。注意manager的自引用外键用了SET_NULL,因为主管离职了不能把下属客户全删掉,保留外键置空最安全。这一节学明白,后面改权限才不至于把视图函数改成一团乱麻。

3. 用Django在本地跑通CRM:环境准备、迁移与最小启动命令

3.1 Python环境与依赖安装:先创建虚拟环境

无论从网上下载的免费Python源码大全里的哪个项目,第一步都是把Python环境准备好。如果你卡在python安装这一步,先确认命令行能敲出python --version,能输出版本号就继续,否则回到Python官网按安装包一路下一步,注意勾选“Add Python to PATH”。这里不推荐在系统里直接pip install,因为Django-CRM及其依赖会把全局环境搅乱,翻车后很难回头。

# 创建虚拟环境并激活 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖,网络慢可以加 -i 镜像参数 pip install -r requirements.txt

venv是Python自带的虚拟环境工具,不需要额外装包。激活之后命令行前缀会出现(venv),这是你在“隔离环境”里的标志。-r requirements.txt会按文件里的依赖列表逐个安装,这套CRM常见的依赖包括Django、djangorestframework、django-filter、django-crispy-forms、requests这几个。我见过不少新手把Django-CRM跑不起来的原因,就是依赖里混入了跟当前Python版本不兼容的包,所以装完可以用pip list看一眼版本。

3.2 数据库配置与settings里的关键开关

源码里的settings.py是整个项目的黑匣子入口。Django-CRM项目默认用SQLite起步,这对本地开发和演示足够,也不需要单独装数据库服务。打开settings能找到DATABASES配置块、INSTALLED_APPS列表和AUTH_USER_MODEL,这三个地方决定了系统能不能跑得通。

# crm_project/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'accounts', 'customers', 'contracts', ] AUTH_USER_MODEL = 'accounts.User' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } }

INSTALLED_APPS里的accounts、customers、contracts就是这套CRM的业务模块。Django的App划分讲究“一个App一个业务域”,accounts管用户和权限,customers管客户与跟进,contracts管合同与审批。AUTH_USER_MODEL必须指向accounts.User,如果写错或漏写,后面migrate时报错根本看不懂。DATABASES里SQLite的路径用BASE_DIR拼接,保证不管从哪个目录启动都找得到数据库文件。

3.3 迁移与超级用户:从源码到能登录后台的完整命令

环境就绪后就到了最刺激的环节——执行迁移。这里给出一套我常用的最小启动命令序列,按顺序敲完就能在浏览器里看到CRM登录页。

# 1. 检查项目配置是否正确 python manage.py check # 2. 根据模型生成迁移文件(源码自带迁移就不用执行) python manage.py makemigrations # 3. 把迁移落到数据库,建表 python manage.py migrate # 4. 创建管理员账号 python manage.py createsuperuser # 5. 启动开发服务器 python manage.py runserver 0.0.0.0:8000

python manage.py check是白捡的一次体检,配置错误会在这一步暴露。makemigrations只生成迁移脚本不碰数据库;migrate才是真正建表的步骤。如果源码自带迁移文件,makemigrations会提示“No changes detected”,直接跑migrate即可。createsuperuser引导你输入用户名、邮箱、密码,这个账号就是后台管理员。runserver加0.0.0.0:8000是让局域网内其他机器也能访问,方便给同事演示。如果浏览器访问http://127.0.0.1:8000/admin/能看到登录页,说明整套源码已经活了。

3.4 进入后台验证CRM首个业务闭环

跑通Django-CRM的源码,不是看到登录页就算完,至少要验证一个完整业务闭环能走通。Django后台是一个现成的管理界面,Access到admin后注册模型就能开始录入数据。但源码里如果没把模型注册到admin,后台只能管理用户和分组,看不到CRM的核心表。这时需要用django创建app的思维方式补上注册文件:

# customers/admin.py from django.contrib import admin from .models import Customer, FollowUp class FollowUpInline(admin.TabularInline): model = FollowUp extra = 0 readonly_fields = ('created_by',) @admin.register(Customer) class CustomerAdmin(admin.ModelAdmin): list_display = ('name', 'status', 'owner', 'created_at') list_filter = ('status',) search_fields = ('name',) inlines = [FollowUpInline]

这段代码把FollowUp以行内编辑的形式嵌套在客户详情页,点进一个客户就能直接添加跟进记录,而不必跳到单独的页面。extra = 0控制不默认显示空行,readonly_fields = ('created_by',)防止手工伪造记录人。list_filter和search_fields是后台列表页的标配,能让销售在客户列表里快速筛出自己的线索。在后台创建一个客户,再添加一条跟进记录,这个动作就是完整的CRUD闭环,模型、迁移、权限、admin都没问题,后续写页面才有底气。

4. 二次开发必动的三块:权限、审批流与消息通知的改造点

4.1 按钮级权限:把Django的has_perm用到任意URL和按钮

Django-CRM拿来即用是理想状态,现实里老板会给各种幺蛾子需求:销售主管要能删除跟进记录,普通销售只能看自己的客户。Django自带的权限体系是按“模型级别”控制的,只能控制到“能不能删客户”这一层,控制不到“能不能删某个客户”。要做的第一件事就是给视图函数加权限装饰器。

# customers/views.py from django.contrib.auth.decorators import permission_required from django.shortcuts import get_object_or_404, redirect from .models import Customer @permission_required('customers.delete_customer', raise_exception=True) def customer_delete(request, pk): customer = get_object_or_404(Customer, pk=pk) customer.is_active = False customer.save() return redirect('customer_list')

permission_required第一个参数是权限码,格式是app_label.codename,customers.delete_customer对应自定义模型里Customer的删除权限。raise_exception=True表示没权限时直接403而不跳登录页,适合页面已登录但对资源越权的场景。模板里配合条件渲染,没权限的销售干脆看不到删除按钮,而不是点进去再吃403。

{% if perms.customers.delete_customer %} <a href="{% url 'customer-delete' customer.pk %}" class="btn btn-danger">删除客户</a> {% endif %}

模板变量perms是Django在请求上下文里自动注入的,遍历当前用户所有权限,不需要额外传参。如果你发现权限码记不住,去admin后台的“用户 → 权限”里能看到一张权限码清单,勾选保存即可,不用写代码。这套组合拳能覆盖绝大多数“按钮级权限”需求。

4.2 合同审批流:用状态字段加信号完成两级审批

合同审批是Django-CRM里绕不开的硬骨头。销售提报合同后,上级要审批、财务要确认,每一步都要留痕。常见做法是给Contract模型加approval_status字段:draft(草稿)、pending(待审批)、approved(已通过)、rejected(已驳回),再加一个current_level表示当前审批到第几级。状态流转本质是CRUD里的更新操作,不需要引入复杂的工作流引擎。

# contracts/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from .models import Contract from accounts.models import User @receiver(post_save, sender=Contract) def on_contract_created(sender, instance, created, **kwargs): """合同创建并进入待审批状态后,通知所有主管""" if created and instance.approval_status == 'pending': managers = User.objects.filter(role='manager', is_active=True) for manager in managers: # 这里可以发站内信、邮件或调用消息通知 create_approval_task(manager, instance)

信号post_save在模型保存后被触发,created=True表示是新插入记录而不是更新。横向比较,我一般不用重写save()方法去做这件事,因为信号更解耦——业务视图、脚本导入、admin后台录入都会触发,不会漏发通知。但也别在信号里写重量级逻辑,比如同步请求外部接口,否则页面保存时卡顿、排查问题像追黑匣子一样痛苦。这个函数里的User.objects.filter(role='manager')用到了第2章的自定义用户模型,查询条件是ORM的原生字段过滤,这就是Django执行查询最标准的写法。

4.3 消息通知:通过Webhook把跟进提醒推到企业微信或钉钉

销售最烦的事情是隔天忘了跟进客户。Django-CRM在FollowUp.next_time里存了下次跟进时间,要做的是定时扫描并把提醒推送到销售手机。实现门槛最低的方式是调用企业微信群机器人Webhook,公司内大多数团队都有这个基础设施。

# common/services.py import requests def push_webhook(webhook_url, content): """推送文本消息到群机器人""" payload = { "msgtype": "text", "text": {"content": content} } resp = requests.post(webhook_url, json=payload, timeout=5) resp.raise_for_status() return resp.json() def send_follow_up_reminders(date_today): """查询需要今天跟进的客户并推送提醒""" from customers.models import FollowUp follow_ups = FollowUp.objects.filter( next_time__date=date_today, is_done=False ).select_related('customer', 'created_by') for follow_up in follow_ups: sales_name = follow_up.created_by.username customer_name = follow_up.customer.name push_webhook( follow_up.created_by.webhook_url, f"{sales_name} 今天需要跟进客户:{customer_name}" )

timeout=5是必须的,Webhook服务万一挂掉,不能让CRM的定时任务卡死在那里。raise_for_status()会在HTTP状态码非2xx时抛异常,方便定时任务把异常捕获记录下来。查询用next_time__date=date_today把日期比较交给数据库处理,比把整个next_time拉回Python再判断高效得多。select_related一次性把外键对象查出来,避免每条记录都发一次SQL,数据量几百条时体感差别就在这次优化里。

4.4 自定义字段与表单的扩展做法

几乎所有CRM二次开发最后都会撞上同一个需求:销售说“我要在客户资料里加一个‘行业’字段”,过两周又说“再帮我们加一个‘客户来源’字段”。如果每次都加一个CharField然后migrate,数据库会越来越碎,迁移文件多到不想看。一套稳妥的策略是用JSONField保存扩展属性。

# customers/models.py class Customer(models.Model): # ... 原有字段 ... extra_fields = models.JSONField(default=dict, blank=True, verbose_name='扩展字段') @property def industry(self): return self.extra_fields.get('industry', '') @industry.setter def industry(self, value): self.extra_fields['industry'] = value

JSONField在Django 3.1之后对SQLite、PostgreSQL都有完整支持。@property把industry伪装成普通属性,表单和模板里可以像访问正常字段一样用customer.industry,但数据库层面不会新增列。这个“软字段”的做法牺牲了一点查询性能——没法用filter(industry='互联网')直接查,需要查extra_fields__industry='互联网',在PostgreSQL上可以走GIN索引弥补。所以我的建议是:高频查询且需要排序的字段用硬列,低频扩展属性进JSON。

5. Django-CRM排错避坑:5个高频翻车场景与修复记录

5.1 现象:migrate 时报 InconsistentMigrationHistory

拿到源码后高高兴兴敲python manage.py migrate,结果报django.db.migrations.exceptions.InconsistentMigrationHistory,这是我见过数量最多的翻车现场。原因几乎都是项目里带着一份旧的db.sqlite3文件,数据库里的迁移记录和源码的迁移文件对不上。源码发布者自己开发时跑过多次迁移,发布时把本地数据库也打进了压缩包,新环境拿到就直接用了旧的迁移历史。

解决方法是备份旧库后彻底重建:把db.sqlite3重命名留底,再重新执行迁移。不要在旧库上强行migrate --fake,那会把迁移历史改乱,后面每次改模型都报错。

mv db.sqlite3 db.sqlite3.bak # 后悔药,出问题还能救回来 python manage.py migrate python manage.py createsuperuser

5.2 现象:AUTH_USER_MODEL 配置晚了很多迁移报错

有认识的新手把Django-CRM跑起来后,想给User加个手机号字段,改完accounts/models.py执行迁移,报错里夹着ValueError: The field admin.logentry.user was declared with a lazy reference to 'accounts.user'之类的一长串。原因是项目启动后已经在django内置的auth模型上建了表,再切换AUTH_USER_MODEL是Django官方都不支持的操作。解决方式只有一条:在第一次migrate前把AUTH_USER_MODEL = 'accounts.User'写进settings,然后删掉旧库重建。如果这套源码本身没设AUTH_USER_MODEL而你又要加顺客字段,我建议新建一个项目把业务app迁过去,比在错误道路上修修补补更快。

5.3 现象:列表页翻页越来越慢

客户数据攒了三个月,销售在列表页翻页开始卡顿,查看SQL日志发现每次把整张表都查了一遍。常见原因有两个:一是Customer.owner外键没有索引,filter(owner=request.user)全表扫描;二是status字段没有建索引,公海页按status='potential'筛选时同样低效。解决方法是给外键和状态字段加db_index=True,然后执行一次迁移。

# customers/models.py owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='customers', db_index=True, # 关键参数,按销售查客户时走索引 ) status = models.CharField( max_length=20, choices=STATUS_CHOICES, default='lead', db_index=True, )

在数据量过万之后,db_index=True是性价比最高的优化,不用改任何查询代码。但注意索引不是万能的:如果你在status上建了索引又在查询里写status__icontains='a',走不上索引,等于白建。这条能救回列表页,但救不回偷懒的查询写法。

5.4 现象:静态文件全部404,admin后台裸奔

开发环境一切都正常,一部署到生产就把DEBUG=False改掉,然后整个后台和页面样式全部炸掉,浏览器控制台一片404。原因在于Django的开发服务器会自动服务静态文件,但生产模式要求你把静态文件收集到一个目录里由Nginx托管。我见过有人迁怒于“服务器环境有问题”,折腾半天发现就是配置缺行。

# settings.py STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [BASE_DIR / 'static']

部署时依次执行python manage.py collectstatic,把staticfiles目录同步到Web服务器。记住STATIC_ROOT是收集目录,STATICFILES_DIRS是源码目录,两者不要指到同一个地方,否则collectstatic会报递归复制错误。这个坑几乎每个项目都会踩一次,踩完就长记性了。

5.5 现象:删除客户触发外键保护无法删除

清理测试数据时想删掉一个客户,HUI到ProtectedError: Cannot delete some instances of model 'Customer' because they are referenced through protected foreign keys。这是我故意用PROTECT设计外键带来的安全副作用。原因在上面models.py里解释过:客户名下挂了跟进的合同,PROTECT会拦截删除。解决不是把外键改成CASCADE图省事,而是按业务顺序手动清理关联数据。

# 先看这个客户名下有哪些不可删的关联 customer = Customer.objects.get(id=1) print(customer.follow_ups.count()) print(customer.contracts.count()) # 确认是测试数据后,一并清理 customer.follow_ups.all().delete() customer.contracts.all().delete() customer.delete()

那条外键的PROTECT是你当初设计时想要的保护,不要因为这次报错就把它改成CASCADE,否则生产环境误删客户会把所有合同一次性清空,连回头的机会都没有。

6. 用Gunicorn加Nginx把CRM跑成永久在线服务

本地跑通只是开始,老板要的是“永久在线的CRM网站”。生产环境最常见的方案是Gunicorn加Nginx,Gunicorn作为Django的WSGI服务器,Nginx在前面指静态文件和反向代理。Gunicorn命令本身留有余地,我先给最小启动:

/opt/crm/venv/bin/gunicorn crm_project.wsgi:application --workers 3 --bind 127.0.0.1:8000

crm_project.wsgi:application指向项目入口,--workers 3在4核以内服务器上是够用的起步值,不用照搬网上“2*CPU+1”的说法,太玄学。--bind 127.0.0.1:8000表示只监听本机,Nginx在本机转发达8080端口,避免把Django直接暴露到公网。为了让它开机自启且崩溃自动拉起,我习惯写一个systemd服务单元:

[Unit] Description=Django CRM Service After=network.target [Service] User=www-data WorkingDirectory=/opt/crm ExecStart=/opt/crm/venv/bin/gunicorn crm_project.wsgi:application --workers 3 --bind 127.0.0.1:8000 Restart=always [Install] WantedBy=multi-user.target

这套配置几乎不用改就能跑,Restart=always是爽跌幅保障,Gunicorn进程死掉systemd会自动拉起来。数据量真的涨到几十万条再考虑上PostgreSQL和Redis,不要在项目第一天就引入复杂组件。我的一个教训是:部署完成后务必用curl -I http://127.0.0.1:8080/验证Nginx转发,别盯着浏览器,先测后端再测前端,否则你会浪费一整个下午在排查一个根本没启动的服务上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询