简介:这是一款基于Django框架的CRM客户关系管理系统,面向需要搭建或二次开发客户管理工具的Python开发者,提供了一套可直接运行和改造的完整项目工程。资源包共652个文件,大小约10.1MB,包含204个Python脚本、202个产品/界面图片、84个PNG图标、67个HTML页面、16个JavaScript文件和13个CSS样式表,另有Dockerfile、配置文件及文档等,覆盖后端逻辑、前端展示、UI素材与部署说明,适合学习Django项目结构和企业级CRM功能模块设计。目前已有550人学习/下载,可用于课程设计、毕业设计或轻量级CRM系统快速原型开发。源码中包含完整的数据模型、视图与模板、静态资源及环境配置,读者可从中掌握Django MTV架构、ORM操作、后台管理等关键技能,并借助打包好的图片与样式资源直接调整界面。1. 一套能做业务闭环的Django CRM,到底在解决什么问题用 Python 与 Django 开发的 CRM 系统完整源码,核心价值不是漂亮的客户列表页,而是把线索、客户、跟进、合同、回款串成一条可追踪的业务链。我见过太多团队把 CRM 做成花哨的通讯录,录完电话就再没人打开——真正的 Django CRM 源码要回答三个问题:谁该跟进这个人、上一轮聊到哪了、这一单到底值不值得追。这篇文章会拆解一套典型 Django CRM 源码的选型逻辑、数据模型、核心模块实现方式、生产部署步骤与二次开发技巧,适合中小团队的技术负责人、刚接触 Django 项目的开发者,以及准备在 CRM 方向上做定制交付的外包团队。跟着走一遍,你就能判断这套源码的边界在哪、落地要补哪些活。2. 为什么是 Django:CRM 系统的技术选型与核心数据模型2.1 选型逻辑:Django 在 CRM 场景里的三个不可替代优势CRM 本质是「多人协作 + 频繁增删改查 + 权限隔离 + 报表统计」的业务系统,和内容站、接口服务不一样。常见的技术栈对比里,我一般会这样选:Flask 和 FastAPI 适合做高并发 API 或微服务,但 CRM 需要一大堆后台管理页面、批量导入导出、复杂关联查询,这些从零搭的成本很高;而 Django 自带 Admin 后台、ORM、迁移机制、认证体系和表单框架,恰好命中 CRM 的高频需求。第一个优势是 ORM 的关联查询能力。CRM 里客户、联系人、跟进记录、合同、订单之间的关联关系复杂,用 Django ORM 的select_related和prefetch_related能减少 90% 的 N+1 查询问题。第二个优势是 Admin 系统,业务人员可以直接用后台录入数据,开发只需要注册 ModelAdmin 就能快速搭出可用的管理界面。第三个优势是权限生态,配合 django-guardian 做对象级权限,能让每个销售只看到自己的客户,这在 CRM 场景里是刚需。要注意的是,Django 的模板渲染和 WSGI 部署方式对高并发不友好,但 CRM 业务特征是内部员工使用,并发量通常在几十到几百,完全在 Django 的舒适区内。如果硬要换成前后端分离 + Node 或 Go 重写,反而会把简单问题复杂化。源码选型上,Django 4.x + Python 3.10+ 是当前最稳妥的组合。2.2 核心数据模型:客户、跟进、合同怎么用外键串起来拿到源码后第一步不是看视图,而是读 models.py。一套合格的 Django CRM 源码,核心模型通常包含五个:客户表(Customer)、联系人(Contact)、跟进记录(FollowUp)、合同(Contract)、回款记录(Payment)。它们之间的关联不是靠到处堆外键,而是围绕客户做聚合。下面是一段典型的核心模型代码,注意我加了关键注释:from django.db import models from django.conf import settings from django.utils import timezone class Customer(models.Model): """客户主表:所有业务动作的聚合根""" name = models.CharField('客户名称', max_length=128) phone = models.CharField('联系电话', max_length=32, blank=True) source = models.CharField('来源渠道', max_length=32, choices=[('online', '线上表单'), ('call', '电话邀约'), ('referral', '转介绍'), ('exhibition', '展会')], default='online') level = models.CharField('客户等级', max_length=16, choices=[('A', '高意向'), ('B', '中意向'), ('C', '低意向')], default='C') owner = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name='负责人', on_delete=models.PROTECT, # 防止误删员工导致客户数据悬空 related_name='customers' ) next_follow_time = models.DateTimeField('下次跟进时间', null=True, blank=True) remark = models.TextField('备注', blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['owner', 'next_follow_time']), ] def __str__(self): return self.name class FollowUp(models.Model): """跟进记录:只追加不修改,保证历史可追溯""" customer = models.ForeignKey(Customer, verbose_name='客户', on_delete=models.CASCADE, related_name='follow_ups') content = models.TextField('跟进内容') # 记录沟通要点 next_time = models.DateTimeField('下次跟进时间') created_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='跟进人', on_delete=models.PROTECT) created_at = models.DateTimeField('跟进时间', auto_now_add=True) class Meta: ordering = ['-created_at']这段代码里有几个关键的选型决策。on_delete=models.PROTECT用在负责人字段上,可以在删除员工时阻止误操作,能避免客户跟着被级联删除。next_follow_time加了索引,因为 CRM 首页最常见的查询就是「今天我要跟进哪些客户」,没索引的话数据量上来后会翻车。FollowUp 表设计成只追加不修改,跟进记录属于业务凭证,改历史记录容易出纠纷,所以视图层直接不提供编辑入口。2.3 权限设计:为什么 CRM 必须做对象级权限角色级权限(销售、主管、管理员)解决的是「能进哪个页面」,但 CRM 的硬需求是「能看哪些客户」。同一个销售主管,既要能看到团队所有人的客户,又要不能看到别的部门的数据,这就需要对象级权限。Django 自带的django.contrib.auth只到模型级,所以源码里通常引入 django-guardian 来补充对象级权限。典型用法是在源码的视图里看到这样的代码:from guardian.shortcuts import assign_perm, get_objects_for_user def assign_customer_perms(user, customer): """给某个用户分配单个客户对象的查看与编辑权限""" assign_perm('view_customer', user, customer) assign_perm('change_customer', user, customer) def get_my_customers(user): """查询当前用户有查看权限的客户列表""" if user.is_superuser: return Customer.objects.all() return get_objects_for_user(user, 'view_customer', Customer.objects.all())assign_perm里的view_customer是 Django 根据模型自动生成的权限名,格式是「动作_模型名小写」。需要注意两个细节:第一,用get_objects_for_user而不是手动过滤customer.owner == user,是因为主管和专员之间的授权关系可能动态变化,写死在视图里后期改不动;第二,对象级权限会让数据库多几张权限关联表,查列表页时用get_objects_for_user会多几条 JOIN,量大的时候记得用prefetch_related缓存结果,不然分页接口会很慢。这套权限模型在源码里是骨架级的存在,二次开发加新模块时,必须同步注册 Model 到 guardian 的权限体系里,否则会出现「有菜单但打开没数据」的怪现象。3. 把源码跑起来:项目结构、环境准备与最小启动命令3.1 源码目录结构:三个先要读的文件拿到源码包后,先别急着pip install,花十分钟把目录结构读明白。一套规范的 Django CRM 源码通常会区分「项目配置」和「业务 App」两层,目录看起来是这个样子:django_crm_project/ ├── manage.py # Django 入口,所有命令都从它进入 ├── requirements.txt # 依赖清单:先看版本再装环境 ├── .env.example # 环境变量模板:数据库、密钥、邮箱配置 ├── config/ # 项目配置层 │ ├── settings/ │ │ ├── base.py # 公共配置 │ │ ├── dev.py # 开发环境配置 │ │ └── prod.py # 生产环境配置 │ └── urls.py # 根路由 ├── apps/ # 业务应用层 │ ├── customers/ # 客户管理:模型、视图、表单 │ ├── follows/ # 跟进记录模块 │ ├── contracts/ # 合同与回款模块 │ └── dashboard/ # 统计看板模块 ├── static/ # 静态资源 ├── media/ # 上传文件(头像、合同附件) └── fixtures/ # 初始数据 JSON三个最先要读的文件是:requirements.txt看依赖版本是否和自己本地环境冲突;config/settings/base.py看数据库默认配置、中间件顺序、App 注册列表;.env.example看哪些敏感配置被外部化了。我习惯先把这三个文件打开摆在编辑器里,再动手建虚拟环境,能避免后面一半的报错。3.2 最小启动命令:从源码到页面打开< bash 代码块,包含以下命令和说明:# 1. 进入源码目录,创建虚拟环境(Windows 用 python -m venv venv) cd django_crm_project python3 -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate # 2. 安装依赖:如果有镜像源需求就加 -i 参数,不指定则用默认源 pip install -r requirements.txt # 3. 准备环境变量:把 .env.example 复制成 .env 并填入本地值 cp .env.example .env # 4. 执行数据库迁移:这一步会用 ORM 把模型映射成真实数据表 python manage.py migrate # 5. 创建管理员账号 python manage.py createsuperuser # 6. 启动开发服务器 python manage.py runserver 0.0.0.0:8000第四步的migrate命令很慢是正常现象,因为 CRM 源码的模型数量多、依赖的权限表也多,初次执行通常会生成几十张表。如果执行时出现类似django.db.utils.OperationalError的报错,基本可以定位为数据库连接问题,优先检查.env里的数据库地址、端口、账号密码是否匹配。runserver默认监听 127.0.0.1,加0.0.0.0:8000是为了局域网里其他同事的电脑也能访问,方便验收演示。3.3 加载演示数据:没有数据的 CRM 看不出业务逻辑跑通空数据库之后,页面是能打开,但你看不到「客户列表长什么样」,因为新库一张表都不满。所以源码里通常会带 fixtures 目录,里面放着初始化的演示数据。加载命令很简单:# 加载演示数据:包含了客户、跟进、合同等关联数据 python manage.py loaddata fixtures/demo_data.json # 如果没有自带演示数据,你自己建少量样本: # 在 Django shell 里执行,创建两个测试客户 python manage.py shell -c " from apps.customers.models import Customer from django.contrib.auth import get_user_model u = get_user_model().objects.first() Customer.objects.create(name='演示客户A', phone='13800000001', owner=u) Customer.objects.create(name='演示客户B', phone='13800000002', owner=u) print('done') "loaddata加载的数据必须满足外键约束,所以 fixture 文件里的「引用顺序」很讲究。如果你发现加载一半报DoesNotExist,通常是用户 ID 或客户 ID 对不上,建议清库重新迁移再加载,不要在脏数据上叠加:python manage.py flush # 清空所有表数据(保留表结构)flush 之后loaddata会干净很多。等看到 Dashboard 页面出现统计图表,说明这套 Django CRM 源码已经真正跑起来了,可以进入下一步的核心代码阅读。4. 核心模块的实现逻辑:客户管理、跟进流与统计看板4.1 客户管理模块:分页、搜索与导入导出的边界处理客户管理页是 CRM 使用频率最高的页面,源码里的实现一般会同时用上 Django 的ListView、django-filter和django-import-export。一个典型的视图长这样:from django.views.generic import ListView from django_filters.views import FilterView from .models import Customer from .filters import CustomerFilter class CustomerListView(FilterView): """客户列表:支持过滤、分页、排序""" model = Customer template_name = 'customers/list.html' paginate_by = 20 # 每页 20 条,经验值 filterset_class = CustomerFilter def get_queryset(self): # 关键:只返回当前用户有权查看的客户 qs = super().get_queryset() qs = qs.select_related('owner') # 避免每条记录额外查一次用户表 return qs这里的select_related('owner')是关键,没有这一行,页面每显示一条客户就会多查一次员工表,20 条记录就是 20 次额外 SQL,数据量 500 条后页面会明显变慢。分页paginate_by = 20是 CRM 列表的推荐值,数值太大滚动吃力,太小失去列表页的意义。导入导出这块,常见的踩坑是导入时字段匹配错误。源码里的 resource 文件一般会做白名单校验,只映射表头和模型字段一致的列:from import_export import resources, fields from .models import Customer class CustomerResource(resources.ModelResource): """定义导入导出的字段映射与校验规则""" name = fields.Field(attribute='name', column_name='客户名称') phone = fields.Field(attribute='phone', column_name='电话') class Meta: model = Customer fields = ('id', 'name', 'phone', 'source', 'level') import_id_fields = ('phone',) # 用手机号做唯一键,重复则更新 skip_unchanged = Trueimport_id_fields = ('phone',)的意思是导入时以手机号为唯一标识,同名号码的客户不会新增而是更新,这个参数在业务上特别重要。如果你发现导入后数据翻倍,大概率是源码没设这个字段,需要手动补上。另一个坑是导入大文件时的内存问题,Excel 超过 5MB 就会变慢,经验做法是先按模板拆分,或在前端限制文件大小。4.2 跟进记录模块:防并发覆盖与只读历史的设计跟进模块是 CRM 里最容易翻车的地方,业务矛盾集中在「两个人同时跟进同一个客户」。源码里的常见做法是在写入时用 Django 的select_for_update()做行级锁,把并发问题挡在数据库层面:from django.db import transaction from django.utils import timezone from .models import Customer, FollowUp def add_follow_up(customer_id, user, content, next_time): """新增跟进记录:先锁定客户行,防止竞态条件""" with transaction.atomic(): customer = Customer.objects.select_for_update().get(pk=customer_id) FollowUp.objects.create( customer=customer, content=content, next_time=next_time, created_by=user ) # 同步更新客户表上的下次跟进时间 customer.next_follow_time = next_time customer.save(update_fields=['next_follow_time', 'updated_at']) return customerselect_for_update()必须在transaction.atomic()事务块里使用,否则不起作用。加锁之后,两个销售同时提交时,后提交的人会等待前一个事务结束,数据不会互相覆盖。这个细节在源码里如果没写,后续会出现「客户跟进时间莫名其妙被改回一周前」的奇葩问题。跟进记录的编辑权限一般默认关闭,只允许删除和新增,不允许修改历史内容。原因不是技术做不到,而是业务上说不清楚——如果员工改掉了昨天写的「客户已拒绝」,主管判断依据就没有了。所以源码层面通常把 FollowUp 的update和delete权限只授权给管理员,普通销售只有add权限。这个权限矩阵在二次开发时不要轻易放开,否则到时候审计起来很被动。4.3 Dashboard 统计:三个查询说清业务状态统计看板是管理者的决策入口,但你会发现很多 CRM 源码的 Dashboard 只是把数据拉出来晒在页面上,没有业务解释。好的做法是用 ORM 聚合出「本月新增客户数、各等级客户占比、回款趋势」三个核心指标,配合图表渲染。核心查询代码可以简化为:from django.db.models.functions import TruncMonth from django.db.models import Count, Sum from .models import Customer from apps.contracts.models import Payment def monthly_stats(month_start, month_end): """按月份聚合新增客户数""" monthly = ( Customer.objects .filter(created_at__range=(month_start, month_end)) .annotate(month=TruncMonth('created_at')) .values('month') .annotate(total=Count('id')) .order_by('month') ) return list(monthly) def payment_trend(days=90): """最近 90 天回款总额走势""" trend = ( Payment.objects .filter(paid_at__gte=timezone.now() - timezone.timedelta(days=days)) .annotate(day=TruncMonth('paid_at')) .values('day') .annotate(amount=Sum('amount')) .order_by('day') ) return list(trend)TruncMonth是 Django 内置的数据库时间截断函数,能把时间归一到当月第一天,然后按月份分组。要注意的是,SQLite 后端对时间函数的支持没有 MySQL 完整,如果你为了省事在本地用 SQLite 跑时会发现时区和精确度有问题,建议源码从第一天就配成 MySQL 或 PostgreSQL,减少统计口径的误差。图表部分一般用 ECharts 或 Chart.js,后端只需要返回 JSON 数据即可,不需要在 Django 里做模板渲染。5. 生产级部署与 5 个必踩的坑:从 DEBUG 到 Gunicorn + Nginx5.1 部署结构:Nginx 处理静态文件,Gunicorn 跑 Django 进程开发环境用runserver没问题,但生产环境里它是单线程的,扛不住并发,而且静态文件的处理效率很低。标准的部署结构是 Nginx 在前端接收请求,把动态请求转发给 Gunicorn 的 socket,把静态资源直接返回。Gunicorn 进程数设置有个经验公式:2 * CPU 核心数 + 1,比如 4 核机器就开 9 个 worker。生产环境的启动命令与 Nginx 配置我一般这样写:# 收集静态文件到 STATIC_ROOT,Nginx 才能直接托管 python manage.py collectstatic --noinput # 启动 Gunicorn:绑定本机 8000 端口,不在公网暴露 gunicorn config.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 9 \ --timeout 120 \ --access-logfile /var/log/crm/gunicorn_access.log \ --error-logfile /var/log/crm/gunicorn_error.log# /etc/nginx/sites-available/crm.conf server { listen 80; server_name your_domain_or_ip; # 替换为你的域名或服务器IP client_max_body_size 20M; # 允许上传合同、头像,不设默认只有 1M location /static/ { alias /path/to/django_crm_project/static/; } location /media/ { alias /path/to/django_crm_project/media/; # 上传文件的访问入口 } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }client_max_body_size 20M这个参数很容易被忽略。不写或太小,销售上传合同附件时直接 413 错误,且 Nginx 报错日志里看不到任何 Django 记录,排查半天才反应过来是 Nginx 拦截了。proxy_set_header X-Forwarded-Proto $scheme也很关键,Django 的SECURE_PROXY_SSL_HEADER配置依赖这个头,否则 HTTPS 部署后所有请求会被 Django 当成 HTTP,跳转逻辑会出问题。5.2 部署避坑五条:现象、原因、解决第一条坑:DEBUG=False 后页面全裸,没有 CSS。现象是 Django 页面能打开但样式全丢。原因是DEBUG=False时 Django 不再自动托管静态文件,而你还没执行collectstatic。解决:执行python manage.py collectstatic --noinput,并把 Nginx 的location /static/指向STATIC_ROOT目录。这个坑几乎是 100% 会踩一次,所以源码文档里必须写清楚步骤。第二条坑:日报统计与业务实际差了 8 小时。现象是每天早上看 Dashboard 发现「今日新增客户」数据不对,下午慢慢对上。原因是USE_TZ=True时 Django 的auto_now_add写入的是 UTC 时间,本地时区没配对。解决:检查settings/base.py里TIME_ZONE = 'Asia/Shanghai'是否设置,且确认USE_TZ + TIME_ZONE组合符合业务预期。如果业务强依赖本地时间,可以保持USE_TZ=True但不把TIME_ZONE设为 UTC。第三条坑:两个销售同时编辑同一客户,后保存的人覆盖先保存的人。现象是 A 改了客户地址保存,B 过了几分钟改了备注保存,A 的地址没了。原因是列表页没有做并发版本控制,提交时直接按主键 UPDATE 覆盖。解决:在 Customer 模型加一个version整数字段,每次保存version + 1,更新时带WHERE version = 旧值,影响行数为 0 时提示「数据已被人修改,请刷新后重试」。源码如果没有这个字段,二开时自己补上。第四条坑:上传的合同文件在详情页打不开。现象是文件上传成功,页面上图片/PDF 404。原因是MEDIA_ROOT和MEDIA_URL配置了,但 Nginx 没有加location /media/块,请求全部被转发到 Gunicorn,Django 又会返回 404。解决:检查 Nginx 配置里 media 的 alias 是否写对路径,且目录的用户权限要让 Nginx 进程能读。第五条坑:邮件提醒发不出去,日志显示 SMTP 连接拒绝。现象是跟进提醒配置好了,结果一封邮件没发。原因是源码默认的邮件配置指向localhost:25,本机没有装邮件服务。解决:在.env里配置真实的 SMTP 服务器,如企业邮箱的smtp.exmail.qq.com配合 SSL 465 端口,并设置EMAIL_USE_SSL = True。不要在EMAIL_HOST_PASSWORD里写明文密码,用环境变量加载。6. 二次开发的正确姿势:给客户模块加一个自定义分组字段拿到源码后,你大概率会想加自己的业务字段。最安全、最不破坏原有结构的做法就是「只加字段、不动逻辑」。比如要给客户增加一个「行业分组」字段,用 Django 的迁移系统可以干净地完成,但要记住三个原则:不改已有模型字段名、不删已有表、不在原视图里加业务判断。# 在 customers/models.py 的 Customer 类里新增字段 industry = models.CharField( '所属行业', max_length=64, blank=True, choices=[('manufacturing', '制造业'), ('finance', '金融'), ('education', '教育'), ('other', '其他')], default='other' )然后执行迁移与注册到筛选器:python manage.py makemigrations customers python manage.py migratemakemigrations会自动生成一个以00xx_alter_customer_xxx开头的迁移文件,不建议手动改文件名。迁移完成后,在CustomerFilter里加一个筛选条件,列表页就会自动出现按行业筛选的下拉框:# customers/filters.py 中的 CustomerFilter 增加: industry = django_filters.ChoiceFilter( field_name='industry', label='行业分组' )ChoiceFilter会读取模型里choices选项生成下拉选择器,不需要手动写模板语句。这几个步骤做完,刷新页面就能看到分组筛选生效了,而原有代码一行都没动。这也是我反复强调的:二次开发的第一原则不是「写得快」,而是「将来能回滚」。Django 的迁移记录给了我们后悔药,用python manage.py migrate customers 00xx之前的状态能回退到任意历史版本,我曾在某次改动里靠这个命令把误删的字段恢复了。如果你想让新字段出现在导入 Excel 模板里,在CustomerResource的fields里追加'industry'并在column_name里写中文表头。这是整个二开流程的最后一步,别漏了——漏了的话业务在导入时永远看不到这个分组列,自己还得吭哧吭哧手动逐条改数据,那是最冤的返工。这套流程下来,你已经具备了对这套 Django CRM 源码做持续改动的底气,剩下就是按自己团队的业务逻辑去扩模块。希望帮到你。本文还有配套的精品资源,点击获取
简介:这是一款基于Django框架的CRM客户关系管理系统,面向需要搭建或二次开发客户管理工具的Python开发者,提供了一套可直接运行和改造的完整项目工程。资源包共652个文件,大小约10.1MB,包含204个Python脚本、202个产品/界面图片、84个PNG图标、67个HTML页面、16个JavaScript文件和13个CSS样式表,另有Dockerfile、配置文件及文档等,覆盖后端逻辑、前端展示、UI素材与部署说明,适合学习Django项目结构和企业级CRM功能模块设计。目前已有550人学习/下载,可用于课程设计、毕业设计或轻量级CRM系统快速原型开发。源码中包含完整的数据模型、视图与模板、静态资源及环境配置,读者可从中掌握Django MTV架构、ORM操作、后台管理等关键技能,并借助打包好的图片与样式资源直接调整界面。1. 一套能做业务闭环的Django CRM,到底在解决什么问题用 Python 与 Django 开发的 CRM 系统完整源码,核心价值不是漂亮的客户列表页,而是把线索、客户、跟进、合同、回款串成一条可追踪的业务链。我见过太多团队把 CRM 做成花哨的通讯录,录完电话就再没人打开——真正的 Django CRM 源码要回答三个问题:谁该跟进这个人、上一轮聊到哪了、这一单到底值不值得追。这篇文章会拆解一套典型 Django CRM 源码的选型逻辑、数据模型、核心模块实现方式、生产部署步骤与二次开发技巧,适合中小团队的技术负责人、刚接触 Django 项目的开发者,以及准备在 CRM 方向上做定制交付的外包团队。跟着走一遍,你就能判断这套源码的边界在哪、落地要补哪些活。2. 为什么是 Django:CRM 系统的技术选型与核心数据模型2.1 选型逻辑:Django 在 CRM 场景里的三个不可替代优势CRM 本质是「多人协作 + 频繁增删改查 + 权限隔离 + 报表统计」的业务系统,和内容站、接口服务不一样。常见的技术栈对比里,我一般会这样选:Flask 和 FastAPI 适合做高并发 API 或微服务,但 CRM 需要一大堆后台管理页面、批量导入导出、复杂关联查询,这些从零搭的成本很高;而 Django 自带 Admin 后台、ORM、迁移机制、认证体系和表单框架,恰好命中 CRM 的高频需求。第一个优势是 ORM 的关联查询能力。CRM 里客户、联系人、跟进记录、合同、订单之间的关联关系复杂,用 Django ORM 的select_related和prefetch_related能减少 90% 的 N+1 查询问题。第二个优势是 Admin 系统,业务人员可以直接用后台录入数据,开发只需要注册 ModelAdmin 就能快速搭出可用的管理界面。第三个优势是权限生态,配合 django-guardian 做对象级权限,能让每个销售只看到自己的客户,这在 CRM 场景里是刚需。要注意的是,Django 的模板渲染和 WSGI 部署方式对高并发不友好,但 CRM 业务特征是内部员工使用,并发量通常在几十到几百,完全在 Django 的舒适区内。如果硬要换成前后端分离 + Node 或 Go 重写,反而会把简单问题复杂化。源码选型上,Django 4.x + Python 3.10+ 是当前最稳妥的组合。2.2 核心数据模型:客户、跟进、合同怎么用外键串起来拿到源码后第一步不是看视图,而是读 models.py。一套合格的 Django CRM 源码,核心模型通常包含五个:客户表(Customer)、联系人(Contact)、跟进记录(FollowUp)、合同(Contract)、回款记录(Payment)。它们之间的关联不是靠到处堆外键,而是围绕客户做聚合。下面是一段典型的核心模型代码,注意我加了关键注释:from django.db import models from django.conf import settings from django.utils import timezone class Customer(models.Model): """客户主表:所有业务动作的聚合根""" name = models.CharField('客户名称', max_length=128) phone = models.CharField('联系电话', max_length=32, blank=True) source = models.CharField('来源渠道', max_length=32, choices=[('online', '线上表单'), ('call', '电话邀约'), ('referral', '转介绍'), ('exhibition', '展会')], default='online') level = models.CharField('客户等级', max_length=16, choices=[('A', '高意向'), ('B', '中意向'), ('C', '低意向')], default='C') owner = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name='负责人', on_delete=models.PROTECT, # 防止误删员工导致客户数据悬空 related_name='customers' ) next_follow_time = models.DateTimeField('下次跟进时间', null=True, blank=True) remark = models.TextField('备注', blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['owner', 'next_follow_time']), ] def __str__(self): return self.name class FollowUp(models.Model): """跟进记录:只追加不修改,保证历史可追溯""" customer = models.ForeignKey(Customer, verbose_name='客户', on_delete=models.CASCADE, related_name='follow_ups') content = models.TextField('跟进内容') # 记录沟通要点 next_time = models.DateTimeField('下次跟进时间') created_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='跟进人', on_delete=models.PROTECT) created_at = models.DateTimeField('跟进时间', auto_now_add=True) class Meta: ordering = ['-created_at']这段代码里有几个关键的选型决策。on_delete=models.PROTECT用在负责人字段上,可以在删除员工时阻止误操作,能避免客户跟着被级联删除。next_follow_time加了索引,因为 CRM 首页最常见的查询就是「今天我要跟进哪些客户」,没索引的话数据量上来后会翻车。FollowUp 表设计成只追加不修改,跟进记录属于业务凭证,改历史记录容易出纠纷,所以视图层直接不提供编辑入口。2.3 权限设计:为什么 CRM 必须做对象级权限角色级权限(销售、主管、管理员)解决的是「能进哪个页面」,但 CRM 的硬需求是「能看哪些客户」。同一个销售主管,既要能看到团队所有人的客户,又要不能看到别的部门的数据,这就需要对象级权限。Django 自带的django.contrib.auth只到模型级,所以源码里通常引入 django-guardian 来补充对象级权限。典型用法是在源码的视图里看到这样的代码:from guardian.shortcuts import assign_perm, get_objects_for_user def assign_customer_perms(user, customer): """给某个用户分配单个客户对象的查看与编辑权限""" assign_perm('view_customer', user, customer) assign_perm('change_customer', user, customer) def get_my_customers(user): """查询当前用户有查看权限的客户列表""" if user.is_superuser: return Customer.objects.all() return get_objects_for_user(user, 'view_customer', Customer.objects.all())assign_perm里的view_customer是 Django 根据模型自动生成的权限名,格式是「动作_模型名小写」。需要注意两个细节:第一,用get_objects_for_user而不是手动过滤customer.owner == user,是因为主管和专员之间的授权关系可能动态变化,写死在视图里后期改不动;第二,对象级权限会让数据库多几张权限关联表,查列表页时用get_objects_for_user会多几条 JOIN,量大的时候记得用prefetch_related缓存结果,不然分页接口会很慢。这套权限模型在源码里是骨架级的存在,二次开发加新模块时,必须同步注册 Model 到 guardian 的权限体系里,否则会出现「有菜单但打开没数据」的怪现象。3. 把源码跑起来:项目结构、环境准备与最小启动命令3.1 源码目录结构:三个先要读的文件拿到源码包后,先别急着pip install,花十分钟把目录结构读明白。一套规范的 Django CRM 源码通常会区分「项目配置」和「业务 App」两层,目录看起来是这个样子:django_crm_project/ ├── manage.py # Django 入口,所有命令都从它进入 ├── requirements.txt # 依赖清单:先看版本再装环境 ├── .env.example # 环境变量模板:数据库、密钥、邮箱配置 ├── config/ # 项目配置层 │ ├── settings/ │ │ ├── base.py # 公共配置 │ │ ├── dev.py # 开发环境配置 │ │ └── prod.py # 生产环境配置 │ └── urls.py # 根路由 ├── apps/ # 业务应用层 │ ├── customers/ # 客户管理:模型、视图、表单 │ ├── follows/ # 跟进记录模块 │ ├── contracts/ # 合同与回款模块 │ └── dashboard/ # 统计看板模块 ├── static/ # 静态资源 ├── media/ # 上传文件(头像、合同附件) └── fixtures/ # 初始数据 JSON三个最先要读的文件是:requirements.txt看依赖版本是否和自己本地环境冲突;config/settings/base.py看数据库默认配置、中间件顺序、App 注册列表;.env.example看哪些敏感配置被外部化了。我习惯先把这三个文件打开摆在编辑器里,再动手建虚拟环境,能避免后面一半的报错。3.2 最小启动命令:从源码到页面打开< bash 代码块,包含以下命令和说明:# 1. 进入源码目录,创建虚拟环境(Windows 用 python -m venv venv) cd django_crm_project python3 -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate # 2. 安装依赖:如果有镜像源需求就加 -i 参数,不指定则用默认源 pip install -r requirements.txt # 3. 准备环境变量:把 .env.example 复制成 .env 并填入本地值 cp .env.example .env # 4. 执行数据库迁移:这一步会用 ORM 把模型映射成真实数据表 python manage.py migrate # 5. 创建管理员账号 python manage.py createsuperuser # 6. 启动开发服务器 python manage.py runserver 0.0.0.0:8000第四步的migrate命令很慢是正常现象,因为 CRM 源码的模型数量多、依赖的权限表也多,初次执行通常会生成几十张表。如果执行时出现类似django.db.utils.OperationalError的报错,基本可以定位为数据库连接问题,优先检查.env里的数据库地址、端口、账号密码是否匹配。runserver默认监听 127.0.0.1,加0.0.0.0:8000是为了局域网里其他同事的电脑也能访问,方便验收演示。3.3 加载演示数据:没有数据的 CRM 看不出业务逻辑跑通空数据库之后,页面是能打开,但你看不到「客户列表长什么样」,因为新库一张表都不满。所以源码里通常会带 fixtures 目录,里面放着初始化的演示数据。加载命令很简单:# 加载演示数据:包含了客户、跟进、合同等关联数据 python manage.py loaddata fixtures/demo_data.json # 如果没有自带演示数据,你自己建少量样本: # 在 Django shell 里执行,创建两个测试客户 python manage.py shell -c " from apps.customers.models import Customer from django.contrib.auth import get_user_model u = get_user_model().objects.first() Customer.objects.create(name='演示客户A', phone='13800000001', owner=u) Customer.objects.create(name='演示客户B', phone='13800000002', owner=u) print('done') "loaddata加载的数据必须满足外键约束,所以 fixture 文件里的「引用顺序」很讲究。如果你发现加载一半报DoesNotExist,通常是用户 ID 或客户 ID 对不上,建议清库重新迁移再加载,不要在脏数据上叠加:python manage.py flush # 清空所有表数据(保留表结构)flush 之后loaddata会干净很多。等看到 Dashboard 页面出现统计图表,说明这套 Django CRM 源码已经真正跑起来了,可以进入下一步的核心代码阅读。4. 核心模块的实现逻辑:客户管理、跟进流与统计看板4.1 客户管理模块:分页、搜索与导入导出的边界处理客户管理页是 CRM 使用频率最高的页面,源码里的实现一般会同时用上 Django 的ListView、django-filter和django-import-export。一个典型的视图长这样:from django.views.generic import ListView from django_filters.views import FilterView from .models import Customer from .filters import CustomerFilter class CustomerListView(FilterView): """客户列表:支持过滤、分页、排序""" model = Customer template_name = 'customers/list.html' paginate_by = 20 # 每页 20 条,经验值 filterset_class = CustomerFilter def get_queryset(self): # 关键:只返回当前用户有权查看的客户 qs = super().get_queryset() qs = qs.select_related('owner') # 避免每条记录额外查一次用户表 return qs这里的select_related('owner')是关键,没有这一行,页面每显示一条客户就会多查一次员工表,20 条记录就是 20 次额外 SQL,数据量 500 条后页面会明显变慢。分页paginate_by = 20是 CRM 列表的推荐值,数值太大滚动吃力,太小失去列表页的意义。导入导出这块,常见的踩坑是导入时字段匹配错误。源码里的 resource 文件一般会做白名单校验,只映射表头和模型字段一致的列:from import_export import resources, fields from .models import Customer class CustomerResource(resources.ModelResource): """定义导入导出的字段映射与校验规则""" name = fields.Field(attribute='name', column_name='客户名称') phone = fields.Field(attribute='phone', column_name='电话') class Meta: model = Customer fields = ('id', 'name', 'phone', 'source', 'level') import_id_fields = ('phone',) # 用手机号做唯一键,重复则更新 skip_unchanged = Trueimport_id_fields = ('phone',)的意思是导入时以手机号为唯一标识,同名号码的客户不会新增而是更新,这个参数在业务上特别重要。如果你发现导入后数据翻倍,大概率是源码没设这个字段,需要手动补上。另一个坑是导入大文件时的内存问题,Excel 超过 5MB 就会变慢,经验做法是先按模板拆分,或在前端限制文件大小。4.2 跟进记录模块:防并发覆盖与只读历史的设计跟进模块是 CRM 里最容易翻车的地方,业务矛盾集中在「两个人同时跟进同一个客户」。源码里的常见做法是在写入时用 Django 的select_for_update()做行级锁,把并发问题挡在数据库层面:from django.db import transaction from django.utils import timezone from .models import Customer, FollowUp def add_follow_up(customer_id, user, content, next_time): """新增跟进记录:先锁定客户行,防止竞态条件""" with transaction.atomic(): customer = Customer.objects.select_for_update().get(pk=customer_id) FollowUp.objects.create( customer=customer, content=content, next_time=next_time, created_by=user ) # 同步更新客户表上的下次跟进时间 customer.next_follow_time = next_time customer.save(update_fields=['next_follow_time', 'updated_at']) return customerselect_for_update()必须在transaction.atomic()事务块里使用,否则不起作用。加锁之后,两个销售同时提交时,后提交的人会等待前一个事务结束,数据不会互相覆盖。这个细节在源码里如果没写,后续会出现「客户跟进时间莫名其妙被改回一周前」的奇葩问题。跟进记录的编辑权限一般默认关闭,只允许删除和新增,不允许修改历史内容。原因不是技术做不到,而是业务上说不清楚——如果员工改掉了昨天写的「客户已拒绝」,主管判断依据就没有了。所以源码层面通常把 FollowUp 的update和delete权限只授权给管理员,普通销售只有add权限。这个权限矩阵在二次开发时不要轻易放开,否则到时候审计起来很被动。4.3 Dashboard 统计:三个查询说清业务状态统计看板是管理者的决策入口,但你会发现很多 CRM 源码的 Dashboard 只是把数据拉出来晒在页面上,没有业务解释。好的做法是用 ORM 聚合出「本月新增客户数、各等级客户占比、回款趋势」三个核心指标,配合图表渲染。核心查询代码可以简化为:from django.db.models.functions import TruncMonth from django.db.models import Count, Sum from .models import Customer from apps.contracts.models import Payment def monthly_stats(month_start, month_end): """按月份聚合新增客户数""" monthly = ( Customer.objects .filter(created_at__range=(month_start, month_end)) .annotate(month=TruncMonth('created_at')) .values('month') .annotate(total=Count('id')) .order_by('month') ) return list(monthly) def payment_trend(days=90): """最近 90 天回款总额走势""" trend = ( Payment.objects .filter(paid_at__gte=timezone.now() - timezone.timedelta(days=days)) .annotate(day=TruncMonth('paid_at')) .values('day') .annotate(amount=Sum('amount')) .order_by('day') ) return list(trend)TruncMonth是 Django 内置的数据库时间截断函数,能把时间归一到当月第一天,然后按月份分组。要注意的是,SQLite 后端对时间函数的支持没有 MySQL 完整,如果你为了省事在本地用 SQLite 跑时会发现时区和精确度有问题,建议源码从第一天就配成 MySQL 或 PostgreSQL,减少统计口径的误差。图表部分一般用 ECharts 或 Chart.js,后端只需要返回 JSON 数据即可,不需要在 Django 里做模板渲染。5. 生产级部署与 5 个必踩的坑:从 DEBUG 到 Gunicorn + Nginx5.1 部署结构:Nginx 处理静态文件,Gunicorn 跑 Django 进程开发环境用runserver没问题,但生产环境里它是单线程的,扛不住并发,而且静态文件的处理效率很低。标准的部署结构是 Nginx 在前端接收请求,把动态请求转发给 Gunicorn 的 socket,把静态资源直接返回。Gunicorn 进程数设置有个经验公式:2 * CPU 核心数 + 1,比如 4 核机器就开 9 个 worker。生产环境的启动命令与 Nginx 配置我一般这样写:# 收集静态文件到 STATIC_ROOT,Nginx 才能直接托管 python manage.py collectstatic --noinput # 启动 Gunicorn:绑定本机 8000 端口,不在公网暴露 gunicorn config.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 9 \ --timeout 120 \ --access-logfile /var/log/crm/gunicorn_access.log \ --error-logfile /var/log/crm/gunicorn_error.log# /etc/nginx/sites-available/crm.conf server { listen 80; server_name your_domain_or_ip; # 替换为你的域名或服务器IP client_max_body_size 20M; # 允许上传合同、头像,不设默认只有 1M location /static/ { alias /path/to/django_crm_project/static/; } location /media/ { alias /path/to/django_crm_project/media/; # 上传文件的访问入口 } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }client_max_body_size 20M这个参数很容易被忽略。不写或太小,销售上传合同附件时直接 413 错误,且 Nginx 报错日志里看不到任何 Django 记录,排查半天才反应过来是 Nginx 拦截了。proxy_set_header X-Forwarded-Proto $scheme也很关键,Django 的SECURE_PROXY_SSL_HEADER配置依赖这个头,否则 HTTPS 部署后所有请求会被 Django 当成 HTTP,跳转逻辑会出问题。5.2 部署避坑五条:现象、原因、解决第一条坑:DEBUG=False 后页面全裸,没有 CSS。现象是 Django 页面能打开但样式全丢。原因是DEBUG=False时 Django 不再自动托管静态文件,而你还没执行collectstatic。解决:执行python manage.py collectstatic --noinput,并把 Nginx 的location /static/指向STATIC_ROOT目录。这个坑几乎是 100% 会踩一次,所以源码文档里必须写清楚步骤。第二条坑:日报统计与业务实际差了 8 小时。现象是每天早上看 Dashboard 发现「今日新增客户」数据不对,下午慢慢对上。原因是USE_TZ=True时 Django 的auto_now_add写入的是 UTC 时间,本地时区没配对。解决:检查settings/base.py里TIME_ZONE = 'Asia/Shanghai'是否设置,且确认USE_TZ + TIME_ZONE组合符合业务预期。如果业务强依赖本地时间,可以保持USE_TZ=True但不把TIME_ZONE设为 UTC。第三条坑:两个销售同时编辑同一客户,后保存的人覆盖先保存的人。现象是 A 改了客户地址保存,B 过了几分钟改了备注保存,A 的地址没了。原因是列表页没有做并发版本控制,提交时直接按主键 UPDATE 覆盖。解决:在 Customer 模型加一个version整数字段,每次保存version + 1,更新时带WHERE version = 旧值,影响行数为 0 时提示「数据已被人修改,请刷新后重试」。源码如果没有这个字段,二开时自己补上。第四条坑:上传的合同文件在详情页打不开。现象是文件上传成功,页面上图片/PDF 404。原因是MEDIA_ROOT和MEDIA_URL配置了,但 Nginx 没有加location /media/块,请求全部被转发到 Gunicorn,Django 又会返回 404。解决:检查 Nginx 配置里 media 的 alias 是否写对路径,且目录的用户权限要让 Nginx 进程能读。第五条坑:邮件提醒发不出去,日志显示 SMTP 连接拒绝。现象是跟进提醒配置好了,结果一封邮件没发。原因是源码默认的邮件配置指向localhost:25,本机没有装邮件服务。解决:在.env里配置真实的 SMTP 服务器,如企业邮箱的smtp.exmail.qq.com配合 SSL 465 端口,并设置EMAIL_USE_SSL = True。不要在EMAIL_HOST_PASSWORD里写明文密码,用环境变量加载。6. 二次开发的正确姿势:给客户模块加一个自定义分组字段拿到源码后,你大概率会想加自己的业务字段。最安全、最不破坏原有结构的做法就是「只加字段、不动逻辑」。比如要给客户增加一个「行业分组」字段,用 Django 的迁移系统可以干净地完成,但要记住三个原则:不改已有模型字段名、不删已有表、不在原视图里加业务判断。# 在 customers/models.py 的 Customer 类里新增字段 industry = models.CharField( '所属行业', max_length=64, blank=True, choices=[('manufacturing', '制造业'), ('finance', '金融'), ('education', '教育'), ('other', '其他')], default='other' )然后执行迁移与注册到筛选器:python manage.py makemigrations customers python manage.py migratemakemigrations会自动生成一个以00xx_alter_customer_xxx开头的迁移文件,不建议手动改文件名。迁移完成后,在CustomerFilter里加一个筛选条件,列表页就会自动出现按行业筛选的下拉框:# customers/filters.py 中的 CustomerFilter 增加: industry = django_filters.ChoiceFilter( field_name='industry', label='行业分组' )ChoiceFilter会读取模型里choices选项生成下拉选择器,不需要手动写模板语句。这几个步骤做完,刷新页面就能看到分组筛选生效了,而原有代码一行都没动。这也是我反复强调的:二次开发的第一原则不是「写得快」,而是「将来能回滚」。Django 的迁移记录给了我们后悔药,用python manage.py migrate customers 00xx之前的状态能回退到任意历史版本,我曾在某次改动里靠这个命令把误删的字段恢复了。如果你想让新字段出现在导入 Excel 模板里,在CustomerResource的fields里追加'industry'并在column_name里写中文表头。这是整个二开流程的最后一步,别漏了——漏了的话业务在导入时永远看不到这个分组列,自己还得吭哧吭哧手动逐条改数据,那是最冤的返工。这套流程下来,你已经具备了对这套 Django CRM 源码做持续改动的底气,剩下就是按自己团队的业务逻辑去扩模块。希望帮到你。本文还有配套的精品资源,点击获取