资讯详情

资讯详情

Django-CRM完整源码解析:数据模型设计与性能优化实战

简介这是一套采用高效Python开发、基于Django框架的CRM客户关系管理系统完整源码面向需要搭建或学习Web业务系统的开发者、运维及项目管理人员可用于客户管理、销售进程跟踪、数据统计等场景。压缩包共652个文件、10.1MB主要包含Python脚本、HTML页面、JavaScript与CSS样式、Shell与Dockerfile部署脚本、YAML配置以及RST文档等完整覆盖后端逻辑、前端交互、样式布局与部署配置便于按模块阅读与二次开发。资源中还有大量JPG与PNG图片素材说明系统具备较完备的界面与功能。已有550人学习下载适合希望理解Django项目分层结构、实际业务功能实现或在现有基础上扩展定制的Python开发者。1. 为什么说 Django-CRM 完整源码的价值全在数据模型上用 Python Django 写一套 CRM 完整源码真正拉开差距的从来不是页面漂不漂亮而是数据模型怎么设计、权限怎么下沉、查询怎么不把数据库打爆。我见过太多团队从零自建客户管理系统三个月后集体死在三个坎上字段越加越多逻辑开始绕、列表页越跑越慢销售开始骂、销售和运营对不上数互相甩锅。这套 Django-CRM 完整源码恰恰把这三件事做了前置设计拿来改一改能直接扛住几十个销售同时录客户、写跟进、看漏斗。它适合两类人一类是公司内部要快速交付 CRM 的开发者想少走弯路另一类是已经写过 Django 增删改查、想看看企业级项目如何组织模型与权限的中级工程师。下面我从数据层开始把这套源码里真正值得抄的决策一个个拆开讲。2. 拆解 Django-CRM 的数据层五张核心表决定了整站上限CRM 这类业务系统页面能换肤、按钮能调但表结构一旦定错后面每次加功能都是在漏水的船上打补丁。一套完整的 Django-CRM 源码核心通常落在客户、联系人、商机、跟进记录、员工账号这五类实体上。2.1 客户与联系人为什么必须拆成两张表很多新手写 CRM 最常犯的错是把联系人的手机号直接做成客户表里的字段。看起来省了一张表实际上把公司的“关系管理”做成了“通讯录管理”。客户是一个组织实体有行业、规模、地域属性联系人是客户下面的具体的人有职位、微信、手机号。一个客户可以有多位联系人一个联系人只属于一个客户这是典型的一对多关系。拆成两张表之后后续做“按联系人群发短信”“离职员工客户交接”“统计某客户有多少个决策人”才都有地方落脚。from django.db import models class Client(models.Model): name models.CharField(max_length128, db_indexTrue) industry models.CharField(max_length64, blankTrue) source models.CharField(max_length32, defaultmanual) is_deleted models.BooleanField(defaultFalse, db_indexTrue) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: indexes [ models.Index(fields[source, is_deleted]), ] class Contact(models.Model): client models.ForeignKey(Client, on_deletemodels.PROTECT, related_namecontacts) name models.CharField(max_length64) position models.CharField(max_length64, blankTrue) mobile models.CharField(max_length32, blankTrue) is_primary models.BooleanField(defaultFalse)这段模型有两个容易被忽略的细节。on_deletemodels.PROTECT是为了防止误删客户时把联系人连带删掉CRM 里客户数据是资产宁可让它删不动也不能静默丢数据。db_indexTrue加在name和is_deleted上是因为客户列表页的搜索和过滤最终都会落在 WHERE 条件上不加索引的话数据量到几万条之后列表页就会从秒开变成转圈。联系人表里的is_primary是一个“主联系人”标记业务上非常有用。给客户发合同、对账、催款时系统默认取主联系人而不是让销售每次手动挑人。很多团队把这个业务规则写在视图层每次查询都.filter(is_primaryTrue).first()结果前端多处逻辑不一致。写在模型字段上配合 Article 保存逻辑强制检查才是正路。2.2 商机与跟进记录状态机驱动的业务流转客户进来了但不等于马上签单。中间还要经过需求识别、方案报价、商务谈判、赢单或输单这些阶段这就是商机表存在的意义。商机表挂在客户下面一条客户可以有多个商机因为同一个客户可能同时谈着不同的产品线。跟进记录是另一张关键表每一条记录代表销售和客户的一次实质互动包括电话、拜访、微信沟通、邮件往来。它和商机之间是时间线的关系——销售每做一次动作就往里面追加一条记录商机的status和follow_count跟着变。Django 里最稳妥的追加写法是「先更新父表再插入记录全程包在事务里」。from django.db import transaction from django.db.models import F opportunity Opportunity.objects.select_for_update().get(pkopp_id) opportunity.status new_status opportunity.follow_count F(follow_count) 1 opportunity.save() FollowUp.objects.create( opportunityopportunity, contentcontent, contactcontact, creatorrequest.user, )这里有三层细节值得说明。第一外层包transaction.atomic()保证“改状态 插入记录”要么都成功要么都回滚否则会出现跟进记录写了、商机状态没更新这种数据脏块。第二select_for_update()是给当前行加锁拦住两个销售同时给同一个商机录跟进时的并发覆盖。第三follow_count F(follow_count) 1让累加操作在数据库端完成而不是先把值读出来加 1 再写回去这一步直接消灭了并发下计数丢更新的大坑。这段代码是这套 CRM 源码里最值得背下来的片段之一。2.3 CRUD 之外软删除与审计字段的建模细节业务型系统里删除永远比新增危险。销售手滑删掉一个客户运营下个月做复盘时数据就少了一块。所以完整源码里的核心表几乎不会做物理删除而是用is_deleted布尔字段做软删。查询时统一过滤掉已删除数据但底层数据永远留在库里。除了软删除每张业务表还需要带上创建时间、更新时间、创建人这三个审计字段。这三件套的作用在出问题复盘时才会体现出来销售说“我上周就录了这个客户”一查created_at发现是昨天运营说“这批数据是团队导入的”一查creator发现是某个销售个人行为。Django 里可以封装一个基类统一加这些字段省得每张表都重复写。class BaseModel(models.Model): created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) creator models.ForeignKey(auth.User, nullTrue, on_deletemodels.SET_NULL) class Meta: abstract Trueauto_now_add是插入时自动取当前时间auto_now是每次save()都刷新时间。注意auto_now在update()批量更新时不会生效如果你用了Opportunity.objects.filter(...).update(statuswon)updated_at不会自动变需要手动带上。这算是 Django ORM 的一个小暗坑后面避坑章节还会展开讲。3. 把 CRM 功能模块落到 Django 视图中跟进、商机与权限数据模型定了之后接下来是视图层。一套合格的企业级 CRM视图绝不只是 CRUD它要回答三个问题一次互动怎么记录、数据怎么汇总成看板、不同角色能看哪些数据。3.1 跟进记录的追加逻辑POST 与事务的写法跟进记录的写入接口是 CRM 里调用频率最高的接口之一。一个销售每天新增几十条跟进如果写入逻辑里有隐藏的性能问题数据库会最先扛不住。login_required require_POST def add_followup(request, opp_id): form FollowUpForm(request.POST) if not form.is_valid(): return JsonResponse({code: 1, msg: 参数有误}, status400) with transaction.atomic(): opp Opportunity.objects.select_for_update().get(pkopp_id, is_deletedFalse) opp.status form.cleaned_data[new_status] or opp.status opp.follow_count F(follow_count) 1 opp.save() FollowUp.objects.create( opportunityopp, contentform.cleaned_data[content], contactform.cleaned_data.get(contact), creatorrequest.user, ) return JsonResponse({code: 0, msg: ok})这段代码把上一章的模型设计落到了接口层。require_POST强制写入接口只接受 POST避免有人用 GET 触发状态变更。select_for_update锁行、F表达式做计数累加这两个组合拳在并发场景下保证数据不错乱。有个细节表单校验放在事务外校验失败直接返回不进数据库省掉无谓的连接占用。表单实例化时要注意不要把数据库查询写进表单的__init__或字段的queryset里太频繁。比如联系人下拉框每个销售可见的联系人不同可以在 form 里接收request.user在clean()阶段做权限校验而不是直接Contact.objects.all()。3.2 销售漏斗看板用 Q 对象做多条件聚合老板和销售主管打开 CRM第一件事是看漏斗——各阶段的商机数量和金额合计数。如果这个页面用循环逐条统计数据量一大就卡死。正解是让数据库一次性聚合出结果。from django.db.models import Count, Sum, Q funnel ( Opportunity.objects .filter(ownerrequest.user, is_deletedFalse) .values(status) .annotate( cntCount(id), total_amountSum(amount, filterQ(is_deletedFalse)), ) .order_by(status) )这段 SQL 翻译成数据库动作就是一次GROUP BY status同时算出每个状态的商机数和金额合计。注意Sum(amount, filterQ(...))是 Django 2.0 之后支持的条件聚合写法比先filter再聚合要灵活尤其适合“金额只看未删除商机但数量统计要包含已删除”这类混合口径。order_by(status)保证前端拿到的是状态顺序固定的列表不然每次返回顺序不一致图表会跳。实际写看板时建议再加两个参数时间范围如近 30 天和过滤维度按团队、按个人。把这两个维度加进filter()而不是聚合完在 Python 里筛否则白费了数据库的索引。3.3 数据权限不靠前端藏着靠 ORM 层过滤CRM 里的权限问题比普通博客系统复杂得多。普通系统只需要区分“登录/未登录”CRM 必须区分“这个客户是谁的”。一位销售不应该看到另一位销售的客户主管可以看到下属的全部客户管理员可以看到所有数据。这套层级权限如果只在前端做“菜单隐藏”等于没做——接口被人摸到就能看到全部数据。class ClientQuerySet(models.QuerySet): def visible_to(self, user): if user.is_superuser: return self if hasattr(user, manager_profile): subordinate_ids user.manager_profile.get_subordinate_ids() return self.filter(owner_id__insubordinate_ids, is_deletedFalse) return self.filter(owneruser, is_deletedFalse) class ClientManager(models.Manager): def get_queryset(self): return ClientQuerySet(self.model, usingself._db).filter(is_deletedFalse)把权限下沉到 QuerySet 层是最适合 Django CRM 的做法。视图里只需要Client.objects.visible_to(request.user)每个入口自然带上权限过滤不会出现漏网之鱼。visible_to内部先判断角色超管直接放行主管先查出下属员工 ID 列表再过滤普通销售只看自己的数据。这里最大的工程意义在于团队后面每加一个视图只要约定好用这个 QuerySet就不会再犯“忘记过滤数据范围”的低级错误。权限校验是安全底线宁可写在少数几个统一入口也不要散落在几十个视图函数里各自实现。4. 让 CRM 真正「高效」起来的 4 处关键优化标题里的“高效”不是形容词是硬指标。CRM 列表页的响应时间、看板聚合的查询次数、批量导入是否卡死这些都是用户每天摸得到的东西。下面按优先级从高到低列四个必做优化。4.1 N1 查询是列表页卡顿的元凶select_related 与 prefetch_related 怎么选客户列表页除了显示客户名和行业通常还要显示“有几个联系人”“最近一次跟进时间”。新手代码会写成循环里逐条查询这就是经典的 N1 问题。# 反例循环中查数据库30 条客户触发 90 条 SQL clients Client.objects.filter(is_deletedFalse)[:30] for client in clients: client.contacts.count() client.opportunities.first() # 正解一条 SQL 联合查出联系人一条 SQL 查出商机 from django.db.models import Prefetch clients ( Client.objects .filter(is_deletedFalse) .prefetch_related( Prefetch(contacts, querysetContact.objects.filter(is_deletedFalse)), Prefetch(opportunities, querysetOpportunity.objects.filter(is_deletedFalse).order_by(-updated_at)), )[:30] )select_related和prefetch_related的区别很多人分不清前者用于外键的单对象关联生成JOIN查询适合client.creator这种多对一后者用于一对多或多对多先查主表再查关联表在内存里做组合适合client.contacts这种。列表页带联系人数量属于一对多必须用prefetch_related。用Prefetch对象还能定制子查询比如只取未删除的联系人、商机按更新时间倒序取最新一条。这个细节很重要否则prefetch_related会把全部联系人加载进来——客户 100 个联系人时内存直接爆掉。4.2 热点页面缓存销售漏斗看板不该每次现算漏斗看板是 CRM 里访问频率极高、数据变化频率极低的页面。销售主管一天刷新几十次每次刷新都让数据库重跑一次GROUP BY非常浪费。对这种页面常见做法是用 Redis 缓存结果设一个较短的 TTL比如 30 秒。from django.core.cache import cache import json def funnel_api(request): cache_key fcrm:funnel:{request.user.id} payload cache.get(cache_key) if payload: return JsonResponse(json.loads(payload)) data build_funnel_data(request.user) cache.set(cache_key, json.dumps(data), timeout30) return JsonResponse(data)关键点在缓存键设计里一定要带上request.user.id。因为不同销售权限不同漏斗里的商机列表不同共用同一个缓存键会造成数据串账。严格说带上了负责人 ID 还会把同一条 SQL 的执行结果在不同人之间重复缓存但 CRM 场景里漏斗本身就该按人隔离所以这个键设计是对的。TTL 设 30 秒而不是永久有效是为了配合新增跟进的操作——刚录完一条跟进主管最多等 30 秒就能看到最新数据体验上完全可接受。这种「缓存键 TTL 兜底」的方案比重建一套缓存失效机制要简单得多也是实战里最不容易出 bug 的缓存写法。等到 TTL 不够用、需要「一改就立即失效」时再考虑显式删除缓存键也不迟。4.3 批量导入与邮件提醒交给异步任务队列CRM 里一定有“批量导入客户 Excel”和“给销售发跟进提醒邮件”这类操作。同步执行时导入 5000 条客户要跑几十秒浏览器一直转圈用户以为系统死了。异步任务队列是标准解法。# tasks.py from celery import shared_task shared_task def import_clients_batch(uploaded_file_path, uploader_id): with open(uploaded_file_path, rb) as f: rows parse_excel(f) for row in rows: client, created Client.objects.get_or_create( namerow[name], defaults{industry: row.get(industry, ), creator_id: uploader_id}, )shared_task的写法让任务函数不依赖具体项目配置直接放到任意 Django App 里都能被 worker 发现。get_or_create在这里解决了“重复导入同一家客户”的问题已存在的客户直接跳过只用created标记判断是否新增。任务完成后通过回调通知上传者比如把结果数写进 Redis前端轮询查询。使用异步任务的边界要记住不是所有操作都值得异步。单条记录的增删改查、漏斗看板聚合这种毫秒级操作走异步反而增加复杂度。异步任务适合耗时超过 1 秒的 IO 密集操作典型场景就是 Excel 导入、邮件群发、短信通知、报表导出。4.4 分页列表瘦身用 values 替代完整实体序列化CRM 列表页往往只需要展示几列客户名、行业、负责人、最近跟进时间。但 DRFDjango REST Framework默认序列化器会输出模型的所有字段包括备注、创建人、更新时间等重字段。数据量大时序列化时间可能比查询时间还长。# 反例序列化完整模型30 个字段全输出 # 正例只取列表页需要的 5 个字段 clients ( Client.objects .filter(is_deletedFalse) .values(id, name, industry, owner__username, updated_at) .annotate(last_followupMax(followups__created_at)) .order_by(-updated_at)[:50] )values()指定查询列之后ORM 生成的 SQL 会减少选择的列配合annotate在数据库端算出最近跟进时间得到的就是可以直接丢给前端渲染的轻量字典集合。列表页和详情页的需求天然不同列表页要“快而瘦”详情页要“全而准”没必要让同一个序列化器两头兼顾。这里有个取舍要提醒values()拿出来的是普通字典失去了 ORM 对象的方法和属性。如果列表页还要用模型里的自定义属性建议在values()基础上补充计算而不是为了保留模型方法放弃这个优化。5. Django-CRM 上线踩坑实录5 个必须提前处理的坑代码写得再顺部署上线时该踩的坑一个也躲不掉。下面 5 条是我在模拟项目 X 和某公司内部 CRM 开发中反复遇到的真实问题按“现象 → 原因 → 解决”的格式整理每条都值得你在动手前先看一遍。5.1 软删除过滤器和 Django Admin 不兼容现象重写了自定义 Manager 的get_queryset过滤掉is_deletedTrue的数据后Django Admin 里编辑客户时关联的联系人下拉框里仍然出现已删除的旧联系人而且编辑保存后已删除的客户又“复活”了。原因Django Admin 的关系字段默认使用关联模型的_base_manager而不是自定义的objects管理器。_base_manager不做行级过滤所以无论你的get_queryset怎么写Admin 的关联查询都不会自动带is_deletedFalse条件。解决自定义 Manager 时把过滤逻辑放在自定义 QuerySet 上并在模型里显式声明base_manager_nameclass Client(models.Model): objects ClientManager() base_manager_name objects或者在 Admin 的formfield_for_foreignkey方法里手动过滤。这两种方案选一个就行我推荐第二种因为base_manager_name改了之后某些依赖原始链路的场景比如反向 FK 查询可能受影响。前者是全局生效后者是只影响 Admin 页影响面更可控。5.2 datetime.now() 导致「今日新增客户」统计翻车现象运营反馈“今日新增客户数”显示为 0但列表里肉眼可见今天有销售录入了客户。更诡异的是过了中午 12 点数字反而变正常。原因项目配置了USE_TZTrue数据库里存的created_at是 UTC 时间。而视图里写的datetime.now()返回的是本地时间的 naive datetime直接拿它去过滤数据库字段会错开时区无法匹配。解决统一使用 Django 的timezone.now()并显式构造当日零点from django.utils import timezone from datetime import timedelta now timezone.now() today_start timezone.localtime(now).replace(hour0, minute0, second0, microsecond0) today_start timezone.make_aware(today_start) count Client.objects.filter(is_deletedFalse, created_at__gtetoday_start).count()timezone.localtime()先转成本地时间再replace掉时分支零点最后用make_aware重新附上时区信息。这套操作保证查询条件和数据库存储都是 aware datetime不会出现隐式转换导致的错位。5.3 并发录跟进导致 follow_count 丢更新现象两位销售同时给同一个商机录跟进后台显示跟进记录有两条但商机列表里的“跟进次数”只加了 1少算了一次。原因代码先get出商机对象读出follow_count值Python 层加 1 再save()写回。并发时两条请求同时读到旧值比如 3各自加 1 都写成 4正确结果是 5。解决用第 2 章里展示的F表达式配合select_for_update()。F(follow_count) 1让数据库端完成累加行锁保证同时只有一条请求能修改该行。如果不想用行锁也可以用update(follow_countF(follow_count) 1)做原子更新同样能解决丢更新问题。行锁适合后面还要继续对该行做其他操作的情况原子更新适合“只改这一个字段”的纯净场景。5.4 自定义 Manager 被序列化后丢掉了过滤现象导出客户 JSON 后导入到另一套环境已删除的客户数据全部出现和原环境行为不一致。原因Django 的序列化产物只包含模型字段不包含自定义 Manager 的过滤逻辑导入时直接落库绕过is_deleted过滤。解决序列化数据库对象时显式用Client.objects.all()而不是默认objects。这里的关键认知是objects管理器已经带了is_deletedFalse过滤序列化出的结果自然不含已删除数据。但导入端如果只是普通objects.create()写入的字段完全可控不会误带 deleted 标记。真正危险的是把整个表数据做物理迁移时直接dumpdata全部记录再loaddata这时候软删除状态会被真实还原。所以软删除数据迁移时建议把is_deletedTrue的数据统一排除在迁移范围外。5.5 漏斗看板缓存键没带权限维度数据串了一个月现象销售 A 看到的漏斗金额突然和销售 B 一样或者主管看的数据里出现了其他人的商机。清掉缓存后数据恢复正常过一段时间又串。原因漏斗看板接口加了 Redis 缓存但缓存键只写了接口名比如crm:funnel没有带上用户 ID 或权限维度。每个销售请求都命中同一份缓存数据被最后写入的那个人覆盖。解决缓存键必须包含用户维度代码在第 4 章已给出核心就是cache_key fcrm:funnel:{request.user.id}。如果看板还允许按团队过滤团队维度也要拼进键里。这一条是缓存场景最容易忽视的出现时往往已经线上跑了一个月造成的数据困惑非常难追溯。6. 用 Django Debug Toolbar 给 CRM 做一次 SQL 体检最后分享一个实战中反复用到的验证习惯给 CRM 的每个关键页面做一次 SQL 体检。工具就是 Django Debug Toolbar 的 SQL 面板不需要额外写脚本打开页面就能看到当前请求执行了多少条 SQL、耗时多少、有没有重复查询。体检流程固定成四步。第一步打开 Debug Toolbar 的 SQL 面板只看总量第二步点进页面记录执行前和执行后各有多少条 SQL如果差别大于 10 条说明列表页有隐藏的 N1 问题第三步点开 SQL 记录找到重复出现多次的相同语句把鼠标悬停到左边的调用栈上定位是哪一行代码触发的查询第四步回到代码里用select_related或prefetch_related修复刷新页面再次对比数量与耗时。下面是一个典型的对比记录数据来自对模拟项目 X 的客户列表页优化前后的实测节选页面优化前 SQL 数优化后 SQL 数优化前耗时优化后耗时客户列表30 条/页904约 800ms约 120ms销售漏斗看板81聚合约 300ms约 80ms商机详情带全部跟进153约 450ms约 150ms发现没有优化空间的分布非常有规律列表页和详情页的 N1 是最大的浪费聚合页面反而问题不大。所以体检的优先级也应该按这个顺序来先查高频率访问的列表页再看详情页最后管看板。全局搜索一下代码里有没有在for循环里写objects.get或objects.filter的地方基本就能锁定 80% 的性能问题。一个额外技巧把 Debug Toolbar 的配置改成只在本地环境开启生产环境关闭不然它会往页面上注入大量调试信息影响加载速度。如果线上出了问题需要排查 SQL可以临时开一个只有自己能访问的调试端点截取慢请求日志而不是全量开启工具栏。我在自己的 Django 项目里养成了一个固定习惯每写完一个列表页或详情页的 view先用 Debug Toolbar 过一遍SQL 数超过“主查询 2”就要回头查。这个习惯看起来笨实际省掉了无数次线上“页面怎么突然变慢了”的排查。CRM 这类业务系统数据量一涨SQL 数量就是生死线。希望这些经过验证的做法能帮你在做 Django-CRM 时少踩几个坑把精力留在真正的业务逻辑上。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →