Django-CRM实战:从部署到二次开发的完整指南

📅 发布时间:2026/10/20 11:43:47
Django-CRM实战:从部署到二次开发的完整指南
简介这是一套基于Django框架开发的CRM客户关系管理系统完整源码定位为Python中高级开发者、Web后端学习者及需要快速搭建客户管理系统的团队。项目共包含652个文件压缩包大小10.1MB核心为204个Python脚本覆盖模型、视图、路由与业务逻辑另有202个JPG图片、84个PNG图片、67个HTML模板、16个JavaScript文件、13个CSS样式表构成完整的前端展示与交互体系同时提供Dockerfile、Jenkinsfile、YAML配置和Shell脚本便于部署与自动化集成。已有550人浏览学习适合用于理解Django项目结构、CRM常见模块如客户管理、跟进记录、数据分析的实现套路也可作为毕业设计或企业内部系统的改造基础。内容预览显示资源内置丰富后台样式Bootstrap、FontAwesome、富交互组件等能帮助快速搭建专业界面是一份实用且完整的Python Web开发参考资料。1. 一个自带完整链路的Django-CRM从源码包到能跑的销售中台原本以为Django-CRM只是给增删改查套了一层后台外壳直到把这套完整源码跑起来才看清潜在客户登记、跟进记录、合同审批、回款统计十几个数据表串成一条完整销售链路后台还有按月份和渠道聚合的仪表盘。这里讲的“高效”指的是开发效率Django自带Admin和ORM让大部分接口零代码落地你拿到手改配置就能部署真正的工作在数据模型拆法和权限隔离上。适合小团队快速搭出销售中台也适合刚学Django的开发者拿来拆模型、权限和报表这几块硬骨头。下文只讲怎么把源码跑起来、核心模块怎么看、哪些位置一不留神会翻车以及我踩过几次之后的固定处理习惯。2. 先把系统跑起来环境准备、数据库初始化与后台登录2.1 项目结构概览与技术选型判断拿到源码压缩包不要急着双击打开README先确认manage.py和最外层配置目录在不在。Django项目看多了会形成一种“肌肉记忆”manage.py负责命令行入口配置目录里放着settings和urls功能模块全部挂在下面的独立app里。CRM系统我见过两种组织风格一种是把所有模型塞进一个crmapp文件越来越大最后admin注册和模型混在一起另一种是拆成客户、跟进、合同、统计等多个app迁移文件和URL路由各管各的。这套源码属于后者customer、followup、contract、dashboard四个app各自独立信号处理和自定义权限也在自己的目录里。后期扩展某个模块时不会因为改一个模型触发其他模块的迁移链。技术栈选型也值得先看一眼模型层用class-based view和命名路由说明当前环境至少要Django 3.0以上Python版本建议3.8或更高。数据库默认配置是SQLite本地零依赖就能跑起来真要上生产把settings里的ENGINE换成django.db.backends.postgresql或mysql再把NAME、USER、HOST、PORT这些参数填进去即可。Admin后台是全功能自带的那一套未做深度魔改所以如果你已经很熟悉Django自带后台操作零成本。判断这套源码是否“干净”我通常会做三个快速检查。第一打开models.py看外键是否全部指定了on_delete行为如果还有裸外键迁移时会有隐患第二看settings里有没有把SECRET_KEY写死在源码里如果写死说明这份源码只是给开发者本地用的部署前需要改成环境变量读取第三搜索代码里有没有带内置时区的硬编码datetime.now()因为时区问题会在报表上造成连锁偏移。做完这三个检查心里基本有数。2.2 三分钟启动依赖安装与配置修改常见做法是先把步骤化整为零创建虚拟环境、安装依赖、编辑settings、执行迁移。这里最关键的其实是虚拟环境我见过直接在全局Python里安装依赖结果把系统环境弄乱和系统包冲突后难以排查。python -m venv venv source venv/bin/activate pip install -r requirements.txtvenv目录建议不要提交进版本库。Windows下的激活命令是venv\Scripts\activate和Linux/macOS不一样。requirements.txt里一般会包含Django、数据库驱动、图片处理、白噪声静态文件工具这几类。如果安装psycopg2失败通常是缺少编译工具链换psycopg2-binary往往更省心。依赖装好后先不要急着迁移数据库把settings里最容易出错的两个配置改掉export DJANGO_SECRET_KEYyour-random-secret-key export DJANGO_DEBUGTrue export DJANGO_ALLOWED_HOSTSlocalhost,127.0.0.1对应的settings里需要配置为从环境变量读取常见写法是使用python-dotenv或os.getenv。如果源码里SECRET_KEY是明文替换成上述方式后记得不要把这个密钥提交到公共仓库。ALLOWED_HOSTS如果不加访问时会抛出DisallowedHost异常这是Django刻意做的安全保护不是源码问题。接下来执行数据库初始化和结构检查python manage.py makemigrations --dry-run python manage.py migrate--dry-run的作用是模拟生成迁移文件但不实际写入用来检查模型和迁移文件之间是否有漂移。如果输出显示“No changes detected”说明源码里的迁移文件和现在的模型一致如果有变化说明源码包可能改过模型但没生成新迁移。migrate执行完成后会自动创建Django内置的auth、session、admin所需的数据表。2.3 初始化后台账号并登录Django-CRM没有内置一个密码写死的默认管理员这是对的。最稳妥的方式是自己创建超级用户python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000createsuperuser交互式提示输入用户名、Email和密码密码要满足Django默认的强度校验太简单会报错。启动后打开浏览器访问http://127.0.0.1:8000/admin用刚创建的管理员账号登录。登录进去后第一件事不是到处点而是先确认左侧菜单有没有把customer、followup、contract这几个核心模块都列出来。如果只有User和Group说明对应的admin.py没有被加载去看app目录底下的apps.py和settings的INSTALLED_APPS是否一致。如果源码自带演示数据或fixture文件可在启动前执行python manage.py loaddata demo.json导入一些测试客户和跟进记录这样调试的时候不至于对着空表空想。3. 核心业务模块拆解从客户线索到成交转化的数据链路3.1 客户管理模块字段设计与数据归属客户表是整个CRM的地基字段设计直接决定后续跟进和统计的写法。这类源码里客户模型通常长这样# customer/models.py from django.db import models from django.conf import settings class Customer(models.Model): STATUS_CHOICES [ (new, 新线索), (following, 跟进中), (contract, 已签约), (lost, 已流失), ] name models.CharField(max_length100) phone models.CharField(max_length30, db_indexTrue) company models.CharField(max_length200, blankTrue) source models.CharField(max_length50, choices[ (ads, 广告投放), (referral, 转介绍), (landing, 落地页), ]) owner models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, related_namecustomers ) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultnew) created_at models.DateTimeField(auto_now_addTrue)我经常给身边做CRM的朋友强调两个细节。第一个是owner外键的on_deletemodels.PROTECT它比CASCADE更符合业务逻辑——如果一个销售账号删除了客户数据不能跟着消失PROTECT会让删除动作报错逼你先把客户转移给别人。第二个是phone字段加db_indexTrue因为客户列表和查重都按手机号精确匹配或模糊匹配不加索引到几万条数据后会明显变慢。后台注册部分要善用Django Admin的能力不要全程裸用默认视图# customer/admin.py from django.contrib import admin from .models import Customer admin.register(Customer) class CustomerAdmin(admin.ModelAdmin): list_display (name, phone, company, source, status, owner, created_at) list_filter (source, status, owner) search_fields (name, phone, company)list_filter和search_fields能帮运营侧极大提升效率运行在后台列表页直接产生WHERE条件不会把所有数据load进内存再过滤。这里有个常见误区在search_fields里加外键关联字段时如果不对应关系写全Django会给出TypeError正确的写法是owner__username这种跨关联路径。3.2 跟进记录与销售漏斗状态流转是怎么实现的跟进记录是销售CRM里最能体现数据链路价值的部分。一笔客户从新线索到签约中间可能产生多次电话、拜访和微信沟通如果每次跟进互不关联后期根本看不出销售动作是否有效。源码中通常会把跟进作为一条独立模型挂在客户下# followup/models.py class FollowUp(models.Model): customer models.ForeignKey( Customer, on_deletemodels.CASCADE, related_namefollow_ups ) staff models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.SET_NULL, nullTrue ) content models.TextField() next_contact_at models.DateTimeField(nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at]客户删除时跟进记录也应该消失所以用CASCADE员工离职时跟进记录里的content和created_at还需要保留所以用SET_NULL并允许staff为空。这个取舍是CRM里最常见的外键策略组合。写查询时要注意N1问题。如果循环客户列表再单独查每条客户的跟进历史每页五十条客户就会产生五十多次SQL听起来影响不大但接上后台列表的加载会明显卡顿。正确写法是使用select_related一次性把关联对象带出来followups FollowUp.objects.select_related(customer, staff).all()select_related适合外键这种单值关联内部是在SQL里做JOIN。多对多或反向外键关系要用prefetch_related思路是分两次查询后在Python侧拼装。如果发现后台查询非常慢先打开DebugToolbar看一眼SQL数量不要急着加缓存。销售漏斗的统计一般用Case/When Count完成而不是把状态一个个写在Python循环里from django.db.models import Count, Case, When status_index (Customer.objects .values(status) .annotate(totalCount(id)) .order_by(status))这段倒不是非要在源码里找到而是你自己改报表时一定会用。理解一个原则就好聚合能下推到数据库的别在Python侧做。3.3 合同与回款金额精度与状态机管理合同表是销售链路从“意向”变“落单”的转折点。金额字段是我最关心的地方合理的源码一定不会用FloatField# contract/models.py class Contract(models.Model): STATUS_CHOICES [ (draft, 草稿), (pending, 待审批), (active, 已生效), (finished, 已完成), (void, 已作废), ] customer models.ForeignKey(Customer, on_deletemodels.PROTECT) amount models.DecimalField(max_digits12, decimal_places2) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultdraft) signed_at models.DateField(nullTrue, blankTrue) approval_note models.TextField(blankTrue)DecimalField会比FloatField更稳妥因为金融金额计算不能接受浮点数误差。max_digits12表示最大十位整数和两位小数议价空间通常在千万级以下都够用如果涉及东南亚或不同币种保留4位小数需要自行调整。状态流转比普通页面里的下拉选择要严格得多。不能把status字段暴露给用户任意修改否则会出现“草稿直接变已作废”这种逻辑漏洞。常见做法是在模型里定义状态转换方法def transition_status(self, new_status, operator): allowed_map { draft: {pending}, pending: {active, void}, active: {finished, void}, } if new_status not in allowed_map.get(self.status, set()): raise ValueError(f不允许从 {self.status} 转换到 {new_status}) self.status new_status self.save()这样就把状态机的约束收拢在一处。审批动作通常放在Admin的action里比如“提交审批”“审批通过”每个action调用上面的方法。operator参数可以用来记录审批人和审批时间给后续报表留证据链。3.4 仪表盘与聚合统计报表页面不是用手写循环算出来的CRM系统如果没有报表模块经理就看不到团队业绩和跟进情况。仪表盘里最常用的是按月订单总额、按渠道客户数、按销售员签约量这几类。Django的ORM提供了Trunc函数做时间分组from django.db.models import Sum, Count from django.db.models.functions import TruncMonth monthly_contract ( Contract.objects .filter(statusactive) .annotate(monthTruncMonth(signed_at)) .values(month) .annotate(total_amountSum(amount), contract_countCount(id)) .order_by(month) )TruncMonth会将每个signed_at截断到月份起点再按月份分组生成的SQL是GROUP BY DATE_TRUNC(month, signed_at)数据量小时完全足够。如果数据量上百万行再做这种实时聚合会拖垮数据库这时常见做法是每天凌晨用定时任务把聚合结果写进一张stats_summary表页面只查汇总表。不要在报表模块里一上来就引入Redis缓存先看慢查询落在哪。仪表盘里还要注意一个“隐形坑”把Sum出来的DecimalField在Python侧转float再格式化。我见过有人为了好拼接写float(row[total_amount])结果大金额出现精度偏移展示给老板看时少了几分钱。正确做法是保留Decimal用{:.2f}格式化输出或者直接用模板里的floatformat过滤器。4. 避坑手册Django-CRM部署与开发中的五个拦路虎4.1 SQLite能跑通的代码MySQL迁移时报数值截断现象本地开发用SQLite一切正常代码推到生产用MySQL执行migrate或插入数据时直接抛Truncated incorrect DECIMAL value之类的异常。原因SQLite类型亲和性很宽松字符串进数值字段时它不会立刻报错MySQL则严格得多。最常见原因是模型里DecimalField和IntegerField没有设置默认值而某个表单或脚本往里写空字符串。在SQLite里空字符串被当作0在MySQL里直接变成语法警告或报错。解决如果字段语义允许为空把字段改成nullTrue, blankTrue, default0然后生成迁移python manage.py makemigrations contract python manage.py migrate如果是历史脏数据需要先写一段脚本清洗把空字符串更新为NULL或0再跑迁移。4.2 登录后台后CSS全部丢失页面裸奔现象本地runserver调试时后台是完整的部署到nginx后用域名访问登录页和列表页全部没有样式只剩干巴巴的HTML。原因Django在DEBUGFalse时不再通过runserver自动提供静态文件。如果settings里没有配置静态文件收集路径和中间件admin自带CSS、JS、图片都找不到。解决在INSTALLED_APPS里加入whitenoise中间件里加上whitenoise.middleware.WhiteNoiseMiddleware然后执行收集python manage.py collectstatic确保STATIC_ROOT指向一个nginx可用的本地目录。如果还是缺样式需要检查nginx的location块是否把/static/映射到了STATIC_ROOT。4.3 多人开发时迁移文件冲突导致项目慌现象A同学和B同学同时各加了一个新字段各自生成迁移文件后准备合并时执行makemigrations指向失败了报告里出现一串Leaf node或MigrationGraphError。原因两个迁移文件基于同一历史版本生成了两条分叉Django需要你来解决合并规则而不是像git那样自动merge。解决千万不要删别人的迁移文件再重新生成那样会破坏其他人的迁移链。正确手段是让其中一个分支等待或主动执行python manage.py makemigrations --merge合并生成的迁移文件会继承两个分支执行后两条链路合到一起。这个操作对逻辑冲突无能为力所以最终还要走一遍migrate然后再做一次makemigrations --dry-run确认没有漂移。4.4 跟进时间比实际晚了8小时现象创建的客户和跟进记录在后台显示创建时间是UTC时间总是比电脑上的北京时间少8小时月度报表也因此被切到错误的日期边界。原因Django默认USE_TZTrue如果settings里没有把TIME_ZONE设置为业务时区时间存进数据库是UTC前端渲染时又没做时区转换展示出来就是UTC值。解决在settings里设定TIME_ZONE Asia/Shanghai USE_TZ True代码中不要用datetime.now()统一使用django.utils.timezone.now()。数据库里存的仍然是UTC但Django在写入和读取时会根据当前时区做转换。要注意已有代码里如果有硬编码的datetime.now()必须全部替换否则新旧记录会出现一个多小时偏移。4.5 普通用户登录后能看到所有客户的私密信息现象已经给用户分配了“销售”角色登录后台发现客户列表还是全量显示根本不分归属人。原因Admin自带权限只控制“能不能进入该模型的管理页”不控制“能看到哪些行”。如果没有重写get_queryset默认所有有权限的用户都能看到整张表。解决在CustomerAdmin里重写查询集方法def get_queryset(self, request): qs super().get_queryset(request) if request.user.is_superuser: return qs return qs.filter(ownerrequest.user)这样普通销售进入客户列表时SQL自动带上owner当前用户的条件。要注意list_filter、search_fields、下拉外键仍然引用全部数据如果要做完整行级隔离还需要重写formfield_for_foreignkey把外键下拉也过滤到当前用户的数据集内。5. 二次开发扩展用户档案、字段与只读报表API5.1 扩展User档案用Profile模型而不是修改内置User如果你在部署几个月后想给每个员工增加“所属门店”或“入职日期”字段这时最稳妥的路径不是把AUTH_USER_MODEL改掉因为系统已经迁移完成临时换模型会带来繁琐的数据迁移。常见做法是新建一个Profile模型用一对一关联内置User# accounts/models.py from django.conf import settings from django.db import models class Profile(models.Model): user models.OneToOneField( settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_nameprofile ) department models.CharField(max_length100, blankTrue) employee_no models.CharField(max_length20, blankTrue) created_at models.DateTimeField(auto_now_addTrue)然后通过信号自动创建Profile实例避免每次手动去后台添加# accounts/signals.py from django.db.models.signals import post_save from django.contrib.auth.models import User from django.dispatch import receiver from .models import Profile receiver(post_save, senderUser) def create_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(userinstance)信号模块记得在AppConfig的ready()里import否则不会生效。开发中如果发现创建用户后Profile没同步第一反应不是去看信号逻辑而是检查app的apps.py有没有把signals模块加载进来。5.2 给客户表加自定义字段迁移与后台注册CRM上线后运营总想要更细的客户分组维度比如“渠道活动ID”和“客户等级”。在Django里加字段是顺手的事但有几个操作顺序不要搞反。先在Customer模型里加字段channel_campaign models.CharField(max_length100, blankTrue, verbose_name渠道活动) grade models.CharField( max_length10, choices[(A, A级客户), (B, B级客户), (C, C级客户)], defaultC )然后生成迁移python manage.py makemigrations customer python manage.py migrate customer迁移文件会自动生成如果字段配置了choicesAdmin后台的下拉选项也需要同步更新。在CustomerAdmin里调整list_display (name, phone, grade, channel_campaign) list_filter (grade, source)这里值得注意的是迁移的同时不要忘了给存量数据填充默认值。如果加了nullFalse的字段迁移时Django会询问你输入一个一次性默认值这时如果不想用命令行交互就预先在迁移文件里用RunPython写一段backfill逻辑。5.3 用DRF做一个只读报表API有些场景下CRM要和公司内部数据大屏对接如果直接把Admin后台的HTML页面给大屏去爬既恶心又脆弱。更合适的方式是使用Django REST framework提供几个只读接口。前提是源码的requirements里已有djangorestframework没有则先安装。在dashboard里新建序列化和视图# dashboard/serializers.py from rest_framework import serializers from contract.models import Contract class ContractReportSerializer(serializers.ModelSerializer): customer_name serializers.CharField(sourcecustomer.name) staff_name serializers.CharField(sourcecustomer.owner.username) class Meta: model Contract fields (id, customer_name, staff_name, amount, signed_at)# dashboard/views.py from rest_framework.viewsets import ReadOnlyModelViewSet from contract.models import Contract from .serializers import ContractReportSerializer from rest_framework.permissions import IsAuthenticated class ContractReportViewSet(ReadOnlyModelViewSet): queryset Contract.objects.filter(statusactive) serializer_class ContractReportSerializer permission_classes [IsAuthenticated]最后在dashboard/urls.py里用router注册from rest_framework.routers import DefaultRouter router DefaultRouter() router.register(contract-report, ContractReportViewSet, basenamecontract-report)这样访问/api/contract-report/能拿到一份JSON格式的合同列表。ReadOnlyModelViewSet天然只支持GETPOST这类写操作不会被暴露减少了改数据的入口。整个二次开发阶段最关键的一点是不要把所有模型都交给同一个ViewSet不加过滤地暴露出错。你至少要在queryset上按当前用户所属团队过滤一遍否则报表接口就成了新的数据泄漏口。6. 再往前一步后台一键导出客户数据并附操作留痕把数据导成Excel或CSV给运营做线下分析几乎是每个CRM项目的刚需。很多团队选择在页面里写一个自定义视图手动遍历数据拼文件但这样每次都要写大量样板代码。更省事的做法是利用Django Admin的action机制直接在列表页勾选记录后触发导出。# customer/admin.py import csv from django.http import HttpResponse def export_customers_csv(modeladmin, request, queryset): response HttpResponse(content_typetext/csv; charsetutf-8) response[Content-Disposition] attachment; filenamecustomers.csv writer csv.writer(response) writer.writerow([姓名, 电话, 公司, 状态, 负责人]) for customer in queryset: writer.writerow([ customer.name, customer.phone, customer.company, customer.get_status_display(), customer.owner.username ]) return response export_customers_csv.short_description 导出选中客户为CSV然后把action注册到CustomerAdmin里actions [export_customers_csv]这个action触发后浏览器会直接下载一个customers.csv并且只包含勾选的记录。注意get_status_display()会返回状态对应的中文显示名不要直接写customer.status否则导出的文件里全是英文代码。如果需要导出为Excel.xlsx而不是CSV可以引入openpyxl或pandas但CSV的好处是纯日常环境就能打开不额外依赖。如果你担心运营导出数据后无法追溯操作可以把action改成先写日志再返回from django.contrib import messages def export_customers_csv_with_log(modeladmin, request, queryset): export_customers_csv(modeladmin, request, queryset) from .models import ExportLog ExportLog.objects.create( operatorrequest.user, countqueryset.count() ) messages.success(request, f已导出 {queryset.count()} 条客户记录)这样每次点击导出都会在数据库里留下一条操作记录对销售敏感数据来说是很必要的审计习惯。从那以后我每次拿到别人的Django-CRM源码都会先检查那块看不见的数据导出实验甚至会在部署前重新梳理一遍后台的action列表确保不该被公开的操作没有默认开启。希望帮到你。本文还有配套的精品资源点击获取