Django CRM源码拆解:业务闭环、权限设计与部署实践

发布时间:2026/10/20 12:36:24
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. 为什么是 DjangoCRM 系统的技术选型与核心数据模型2.1 选型逻辑Django 在 CRM 场景里的三个不可替代优势CRM 本质是「多人协作 频繁增删改查 权限隔离 报表统计」的业务系统和内容站、接口服务不一样。常见的技术栈对比里我一般会这样选Flask 和 FastAPI 适合做高并发 API 或微服务但 CRM 需要一大堆后台管理页面、批量导入导出、复杂关联查询这些从零搭的成本很高而 Django 自带 Admin 后台、ORM、迁移机制、认证体系和表单框架恰好命中 CRM 的高频需求。第一个优势是 ORM 的关联查询能力。CRM 里客户、联系人、跟进记录、合同、订单之间的关联关系复杂用 Django ORM 的select_related和prefetch_related能减少 90% 的 N1 查询问题。第二个优势是 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_length128) phone models.CharField(联系电话, max_length32, blankTrue) source models.CharField(来源渠道, max_length32, choices[(online, 线上表单), (call, 电话邀约), (referral, 转介绍), (exhibition, 展会)], defaultonline) level models.CharField(客户等级, max_length16, choices[(A, 高意向), (B, 中意向), (C, 低意向)], defaultC) owner models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name负责人, on_deletemodels.PROTECT, # 防止误删员工导致客户数据悬空 related_namecustomers ) next_follow_time models.DateTimeField(下次跟进时间, nullTrue, blankTrue) remark models.TextField(备注, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) 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_deletemodels.CASCADE, related_namefollow_ups) content models.TextField(跟进内容) # 记录沟通要点 next_time models.DateTimeField(下次跟进时间) created_by models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name跟进人, on_deletemodels.PROTECT) created_at models.DateTimeField(跟进时间, auto_now_addTrue) class Meta: ordering [-created_at]这段代码里有几个关键的选型决策。on_deletemodels.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, phone13800000001, owneru) Customer.objects.create(name演示客户B, phone13800000002, owneru) 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(attributename, column_name客户名称) phone fields.Field(attributephone, 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(pkcustomer_id) FollowUp.objects.create( customercustomer, contentcontent, next_timenext_time, created_byuser ) # 同步更新客户表上的下次跟进时间 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(monthTruncMonth(created_at)) .values(month) .annotate(totalCount(id)) .order_by(month) ) return list(monthly) def payment_trend(days90): 最近 90 天回款总额走势 trend ( Payment.objects .filter(paid_at__gtetimezone.now() - timezone.timedelta(daysdays)) .annotate(dayTruncMonth(paid_at)) .values(day) .annotate(amountSum(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_ROOTNginx 才能直接托管 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 部署避坑五条现象、原因、解决第一条坑DEBUGFalse 后页面全裸没有 CSS。现象是 Django 页面能打开但样式全丢。原因是DEBUGFalse时 Django 不再自动托管静态文件而你还没执行collectstatic。解决执行python manage.py collectstatic --noinput并把 Nginx 的location /static/指向STATIC_ROOT目录。这个坑几乎是 100% 会踩一次所以源码文档里必须写清楚步骤。第二条坑日报统计与业务实际差了 8 小时。现象是每天早上看 Dashboard 发现「今日新增客户」数据不对下午慢慢对上。原因是USE_TZTrue时 Django 的auto_now_add写入的是 UTC 时间本地时区没配对。解决检查settings/base.py里TIME_ZONE Asia/Shanghai是否设置且确认USE_TZ TIME_ZONE组合符合业务预期。如果业务强依赖本地时间可以保持USE_TZTrue但不把TIME_ZONE设为 UTC。第三条坑两个销售同时编辑同一客户后保存的人覆盖先保存的人。现象是 A 改了客户地址保存B 过了几分钟改了备注保存A 的地址没了。原因是列表页没有做并发版本控制提交时直接按主键 UPDATE 覆盖。解决在 Customer 模型加一个version整数字段每次保存version 1更新时带WHERE version 旧值影响行数为 0 时提示「数据已被人修改请刷新后重试」。源码如果没有这个字段二开时自己补上。第四条坑上传的合同文件在详情页打不开。现象是文件上传成功页面上图片/PDF 404。原因是MEDIA_ROOT和MEDIA_URL配置了但 Nginx 没有加location /media/块请求全部被转发到 GunicornDjango 又会返回 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_length64, blankTrue, choices[(manufacturing, 制造业), (finance, 金融), (education, 教育), (other, 其他)], defaultother )然后执行迁移与注册到筛选器python manage.py makemigrations customers python manage.py migratemakemigrations会自动生成一个以00xx_alter_customer_xxx开头的迁移文件不建议手动改文件名。迁移完成后在CustomerFilter里加一个筛选条件列表页就会自动出现按行业筛选的下拉框# customers/filters.py 中的 CustomerFilter 增加 industry django_filters.ChoiceFilter( field_nameindustry, label行业分组 )ChoiceFilter会读取模型里choices选项生成下拉选择器不需要手动写模板语句。这几个步骤做完刷新页面就能看到分组筛选生效了而原有代码一行都没动。这也是我反复强调的二次开发的第一原则不是「写得快」而是「将来能回滚」。Django 的迁移记录给了我们后悔药用python manage.py migrate customers 00xx之前的状态能回退到任意历史版本我曾在某次改动里靠这个命令把误删的字段恢复了。如果你想让新字段出现在导入 Excel 模板里在CustomerResource的fields里追加industry并在column_name里写中文表头。这是整个二开流程的最后一步别漏了——漏了的话业务在导入时永远看不到这个分组列自己还得吭哧吭哧手动逐条改数据那是最冤的返工。这套流程下来你已经具备了对这套 Django CRM 源码做持续改动的底气剩下就是按自己团队的业务逻辑去扩模块。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询