从Excel到自研CRM:Django+PostgreSQL构建企业级客户管理系统实战
2026/9/16 9:46:34 网站建设 项目流程

1. 项目缘起:为什么我会动手写一个DeskcommCRM

1.1 从一张手写Excel表说起

说起来挺有意思,DeskcommCRM这个项目最开始并不是奔着"做一个软件"去的。当时一家做办公设备租赁的客户找到我,说他们手里有三千多个客户,分散在六个销售手里,管理方式是一个人维护一张Excel表,每张表里字段都不一样,有的记了报价日期,有的只写了个公司名和电话号码。更要命的是,这六张表互相之间是没有关联的,一个客户今天被A销售拜访过,明天B销售又打电话过去问同样的问题,客户烦得要命,老板也看不到整个销售漏斗到底健康不健康。

客户最初的需求是"帮我们做个客户登记表,能多人填就行"。但是我在看完他们的实际业务流之后,发现他们缺的其实不是一个表单,而是一个能把客户档案、跟进记录、报价审批、合同回款、售后工单全部串起来的系统。这就是DeskcommCRM的起点。

当时我评估了几个方向:买一个现成的SaaS CRM,一年下来授权费加上实施费,十几个账号也要两三万;用低代码平台搭,虽然便宜,但后续数据导出、权限管控都很受限;最后我决定自己搭一套,把核心功能全部自己掌控。我的选型标准非常明确:好不好部署、容不容易改、数据敢不敢完全放本地,这三点优先级高于一切。

1.2 DeskcommCRM到底解决了什么问题

如果用一句话概括,DeskcommCRM就是"以客户档案为中心,把售前、售中、售后全部串在一起的协作工具"。它解决的核心问题有三个。

第一是信息孤岛。以前销售各自为战,客户数据全在个人电脑里,人走了数据也就没了。现在所有客户统一入库,每个客户都有唯一的归属人,其他人访问要申请权限,操作留痕清清楚楚。

第二是过程管理缺失。以前老板问"这个项目进展怎么样了",销售只能凭记忆说个大概。现在每个客户都有销售阶段、最近跟进时间、下一次计划动作,管道的每个环节有几个商机、总金额多少,点开仪表盘就能看到,不再依赖人脑回忆。

第三是售后跟进断层。设备租赁行业有个特点,设备出故障是有周期的,今天修完了,三个月后可能又出问题。以前维修记录靠微信群聊翻记录,现在每台设备绑定一个客户档案,维修工单从报修到处理到回访都有记录,而且和客户的历史报修记录关联在一起,技术员上门之前就能看到这台设备的"病历"。

这个系统前前后后花了大概三周时间,我把核心模块全部跑通并部署到客户的内部服务器上。到现在已经稳定运行大半年了,三千多个客户、两万多条跟进记录全部沉淀在系统里。我把自己在这个项目里的完整思路、踩过的坑、做过的决策都整理出来,希望能给正准备做类似内部系统的开发者和业务负责人一些参考。

2. 整体架构与核心技术选型解析

2.1 技术栈定型的几个关键决策

DeskcommCRM的技术选型我坚持一个原则:能用成熟方案解决的,不自己造轮子。因为这是一个面向企业内部使用的系统,它的核心竞争力在于业务流程的贴合度,而不是技术有多炫。

后端我选了Python的Django框架,原因是它的ORM和Admin后台太适合这种"表结构清晰、管理功能多"的业务系统了。Django自带一套完整的用户认证和权限框架,开发初期可以省掉大量重复劳动,让我把精力集中在客户管理、商机管理这些核心业务逻辑上。有人可能会问,为什么不选Spring Boot或者Go?原因很简单:这个项目的开发周期只有三周,我需要一个开发效率足够高的框架,而且客户的服务器配置不高,Python应用的资源占用也完全可以接受。

前端方面我用的是Django模板加少量Vue的混合方案。纯粹的模板渲染在交互复杂的地方会很痛苦,而全部上前后端分离对这个规模的项目来说又过于重了。我的方案是:列表页、详情页这些以展示为主的页面用模板直接渲染,客户快速录入、商机阶段拖拽这种交互要求高的模块用Vue写独立的组件,通过接口和Django交互。这样既保证了开发效率,又兼顾了用户体验。

数据库选了PostgreSQL,没有别的原因,就是它的JSON字段和窗口函数太好用了。客户的自定义属性我用一个JSON字段来存,这样不用频繁改表结构;统计报表里的排名、占比计算用窗口函数写起来非常顺畅。后来看到PostgreSQL在处理两万多条跟进记录联表查询时依然能做到毫秒级响应,我更确信这个选择是对的。

2.2 核心表结构设计与业务字段规划

数据库是整个系统的地基,表结构设计得好不好,直接决定了后面开发顺不顺畅。DeskcommCRM的核心表我设计了五张,相互之间的关系我梳理了很久。

第一张是customer客户表。除了公司名称、联系人、电话这些基本字段外,我特别设计了source(客户来源)、level(客户等级)、owner_id(归属销售)这几个业务字段。尤其是owner_id,它是权限控制的关键,每个客户只能有一个归属人,所有数据操作都围绕这个字段做隔离逻辑。

第二张是contact联系记录表。每次销售打电话、发邮件、上门拜访都要在这里留一条记录,关联到具体的客户。这张表的字段比较少,但next_follow_date(下次跟进时间)这个字段很重要,它是销售日常工作的主线索,系统每天会自动汇总当天该跟进的客户列表推送给销售。

第三张是opportunity商机表。商机代表的是一个具体的销售机会,比如客户要租十台打印机和全套的维护服务。商机需要记录预计成交金额、预计签单日期、当前的销售阶段。销售阶段的设计我参考了标准的销售管道模型,但也结合客户的实际情况做了调整,每一步都对应一个成交概率。

第四张是contract合同表,记录最终成交的订单信息,包括合同金额、合同周期、关联的设备清单。第五张是ticket工单表,记录客户报修、技术员上门处理的全过程。

这五张表的关系是:客户一对多联系记录,客户一对多商机,商机一对一合同,合同一对多工单。整个业务闭环就是从客户到商机、从商机到合同、从合同到售后的完整链条。

2.3 为什么选型时避开了大而全的方案

在给客户选型时,我其实动过用那些开源的重量级CRM系统的念头,但深入研究后还是放弃了。最大的问题在于通用型产品要把所有行业的需求都兼容进来,导致功能结构特别复杂,比如有些系统里光是字段类型就有十几种配置方式,还有一堆我用不到的营销自动化模块。

但设备的租赁、维修、续保业务,这些是行业内非常具体的场景,通用型CRM很难覆盖到位。比如设备租赁行业有个"只保内修还是保外也修"的问题,关系到报价方案的设计;又比如"这台设备当前是出租中还是已退库"这种状态管理,通用产品里根本找不到对应的功能。

自己开发最大的好处,就是每一个表、每一个字段、每一个状态机转换都是为业务量身定制的。我不需要花时间去理解一个复杂的通用系统,然后费劲地把它改造得适应业务;我只需要直接按照业务的本来面貌去建模,这样系统上线之后的用户培训成本也会低很多。

3. 核心功能模块实操拆解

3.1 客户管理模块:从客户建档到360度视图

客户管理模块是整个DeskcommCRM的基石,也是我花费精力最多的部分。在设计这个模块时,我最关注的是"客户360度视图"这个概念:在一个页面里,用户能看到这个客户的基本信息、全部跟进记录、在谈商机、历史合同、设备清单、服务工单,以及这个客户给公司带来的累计收入。

客户基本信息的字段设计,我遵循了"必要字段少、可选字段灵活"的原则。必填字段只有公司名称和联系电话两个,因为如果必填项太多,销售录入时会产生很大的抵触心理。其他字段比如客户规模、所属行业、办公地址、设备数量等都放到可选,同时预留了一个JSON格式的自定义字段区域,管理员可以在后台配置额外的扩展属性。

客户列表页的体验我很在意,因为这是销售每天打开频率最高的页面。列表支持按归属人、客户等级、客户来源、最近跟进时间进行筛选,支持按任意字段排序,支持模糊搜索和高级组合搜索。搜索我用了PostgreSQL的全文检索功能,实测在三千个客户里面搜一个关键词大概30毫秒,销售反应很快、很好用。

在建档方式上我做了两种入口。一种是传统的表单录入,适合从市场活动拿到一批线索后手工录入;另一种是Excel批量导入,适合客户把以前的老数据一次性迁移进来。Excel导入这个功能虽然不起眼,但实际落地时坑特别多,我在后面的问题排查部分会专门展开。

3.2 商机管道:销售阶段的流转逻辑与统计口径

商机管理模块的功能是让管理者能一眼看明白销售业绩预期和潜在风险。在客户的需求里,"销售过程是否健康"是比"这周签了几单"更重要的问题,因为销售是有周期的,今天埋下的种子可能要三个月后才能收获。

我把销售管道定义成五个阶段:初步接洽、需求确认、方案报价、商务谈判、赢得/输单。每个阶段对应一个成交概率,初步接洽是10%,需求确认是30%,方案报价是50%,商务谈判是70%,赢得是100%,输单是0%。这个概率不是拍脑袋定的,而是根据这个客户过去一年的历史数据反推出来的:他们过去成交的单子里,平均经过几个阶段、各阶段停留多长时间,都有统计。

商机的阶段流转在前端做了一个拖拽看板,类似于常见的任务管理工具。销售把和客户沟通的进展判断清楚后,直接把卡片从一个阶段拖到下一个阶段,系统会自动记录阶段变更的时间和操作人。这个设计很受销售欢迎,因为以前他们在Excel里根本没法这么直观地管理自己的销售任务。

管道报表是这个模块里的重头戏。我按五个阶段展示了每个阶段的商机总数和总金额,并且计算出每个阶段的转化率。比如初步接洽有20个商机、总额60万,到最后只签了2个,那总转化率就是10%。这个数据老板非常关注,因为他能据此判断是线索质量不行、还是销售跟进的能力有问题。

3.3 工单系统:报修、派单、处理、回访的完整闭环

工单系统严格来说不算CRM标配,但对于设备租赁行业来说,这是客户粘性最直接的体现。设备出了故障能不能快速响应,直接决定了客户续约的时候还愿不愿意继续合作。

DeskcommCRM的工单流程是这样的:客户打电话报修,客服在系统里创建一个工单,关联到对应的客户和设备。工单有三个紧急级别:普通、紧急、特急。特急的规则是设备完全瘫痪导致客户业务中断,系统会自动发短信通知技术主管,要求两小时内到场响应。

派单环节我设计了一个简单的抢单模式:新工单创建后,所有空闲的技术员都能在系统里看到,先点击"接单"的获得这个工单。这个模式比管理员指定的效率高很多,因为技术员最清楚自己手头忙不忙,抢单机制也调动了积极性。当然系统也保留了管理员强制派单的功能,防止某些工单大家都不想接、互相推诿的情况出现。

技术员处理完工单后,需要填写处理结果、更换配件、工时等信息。最后客服要做回访,确认客户对处理结果满意后,这个工单才能关闭。如果回访不满意,工单会自动重新打开并打回给原来的技术员,形成一个闭环。这些工单数据沉淀下来后很有意思,能分析出哪些型号的设备故障率最高、平均维修时长是多少、哪些技术员一次修复率最高,这些分析数据对公司的运营决策非常有价值。

3.4 仪表盘与统计报表:从业务数据到管理决策

每个系统到最后都要回答一个问题:数据怎么变成决策依据。DeskcommCRM的仪表盘分三个视角:老板视角、销售主管视角、销售个人视角。

老板视角的首页展示的是核心经营指标:本月新客户数、本月签约金额、应收账款总额、近六个月签约趋势、各业务员业绩排名。最有价值的是业绩排名的趋势图,能看出来老销售是不是在吃老本、新人的成长曲线怎么样。

销售主管视角的仪表盘更关注团队的过程指标:团队总商机金额、各阶段商机分布、平均成单周期、商机赢率。一套数据下来,主管能定位出团队里哪些商机推进太慢、哪些销售长期在低成交概率阶段徘徊、哪些客户的跟进频率明显不足。

销售个人视角就简单很多:我自己的客户总数、今天的待跟进客户、我的商机总额、我这个月的成交额、我的工单响应速度。个人视角我特意没放任何排名相关的数据,因为排名会让销售产生恐惧和抵触心理,不利于他们准确记录数据。这个细节是客户在使用过程中反馈给我的,我觉得很有道理。

4. 权限模型与数据安全落地

4.1 RBAC权限模型:从粗粒度到细粒度的取舍

客户管理系统的权限设计是一个既重要又容易出问题的地方。DeskcommCRM采用经典的RBAC(基于角色的访问控制)模型,角色分为三种:系统管理员、销售主管、普通销售,另外还有一种只读访客角色,给老板的财务或者外部审计用。

角色的权限配置上,我做了几个精细的区分。普通销售只能看到自己名下的客户、商机、工单;销售主管能看自己团队的全部数据;系统管理员能看整个公司的全部数据。在操作权限上,普通销售只能创建和编辑自己的数据,不能删除任何记录(删除权限只有系统管理员有,而且所有删除操作都会在后台留下日志,防止误删和滥用)。

一个比较难拿捏的边界是公海客户的可见性问题。有的公司客户和销售的关系非常紧密,有的公司则希望客户完全属于公司、销售只是代为跟进。DeskcommCRM的解决办法是增加一个"客户池"的概念:销售离职或者长期不跟进(超过30天),客户会自动回到客户池,其他销售可以申请领取。申请领取的审核人是销售主管,主管确认后客户归属变更,且变更记录永久留存。这套机制既保护了公司的客户资产,又不至于让销售产生"我一旦不跟进客户就不是我的了"的焦虑,因为规则是明确的、自动化的。

4.2 数据隔离与操作留痕

数据安全分为两部分:一部分是数据隔离,另一部分是审计追踪。

数据隔离的核心是行级权限。Django的ORM天然支持自定义查询集的过滤逻辑,我在基类管理器里重写了get_queryset方法,根据当前登录用户的角色自动追加过滤条件,例如销售角色会自动加上owner_id=request.user.id。这样做的好处是权限控制不允许绕过——就算有人手工构造URL去访问别人的客户详情页,查询集返回的数据也是空的,从源头杜绝了越权访问。

操作留痕方面,我用了Django的signals机制,在客户、商机、合同、工单这四个核心模型的增删改操作上全部挂上了审计钩子。每次修改,系统自动记录「谁在什么时间把哪个字段从什么值改成了什么值」。这个审计记录不仅能满足内部风控的需求,还能在客户纠纷、销售对比时作为有效证据。

数据库层面做了每日自动备份,保留最近三十天的备份文件,同时备份文件自动同步到另一台独立的存储设备上。有一次客户误操作删了一批跟进记录,就是靠备份恢复回来的,从那以后他们对这套系统的信任度明显提升了很多。

4.3 登录安全与运维细节

登录模块我加上了一些必要的安全设置。一是账号锁定策略:同一个账号连续输错五次密码,锁定十五分钟。二是会话超时设置:登录状态超过两小时没有任何操作,自动退出。三是所有密码用Django内置的PBKDF2算法加盐哈希后存储,绝对不保存明文密码。

服务器方面部署在客户内网的一台CentOS机器上,用Nginx作为反向代理,Gunicorn作为WSGI服务,PostgreSQL和系统跑在同一台机器上。为了规范地管理这些进程,我特意用systemd写了一个服务单元文件,开机自启、崩溃自动重启都配置好了。因为是在内网运行,我没有启用HTTPS,但如果将来系统要映射到公网使用的话,这一步千万不能省,毕竟现在传输的数据里有客户联系方式、合同金额这些敏感信息。

这里要特别补充一个我在多个项目里反复验证的经验:内部系统的安全策略,一定平衡好安全性和易用性。比如十五分钟无操作自动退出这个策略,如果任务比较重、经常要跨段时间才能回来操作,就会觉得这个策略过于严苛;但如果完全没有超时策略,又存在安全风险。这类参数我通常都做成了后台可配置项,让用户根据自己的实际情况调整。

5. 实操过程中的典型问题与排查经验

5.1 Excel批量导入的编码与校验陷阱

前面提到批量导入功能,这绝对是个看起来容易、做起来坑很多的模块。第一次测试我就碰到了一个很典型的问题:客户发来一份Excel,里面有客户的备注信息是中文的,我写了个简单的脚本直接读取入库,结果跑完发现所有中文备注全部变成了乱码。

排查下来发现原因在于Excel文件的编码格式不对。用pandas读取Excel时,默认会尝试用UTF-8解码,但客户那份文件是早期的Excel 97格式,存储编码是GBK。解决方案是读取文件时显式指定编码参数,或者在导入时做一次编码探测,自动识别文件编码再转成UTF-8入库。但这里我必须提醒一句,不能完全依赖编码探测,因为有些文件本身就是混合编码的,最稳妥的方案是在导入前先让用户预览解析结果,确认无误后再真正执行入库。

第二个坑是数据校验。Excel里的手机号往往会被Excel软件自动转成科学计数法,比如13812345678显示成1.38123E+10,读进来的时候如果没处理,就会导致手机号字段变成了一个浮点数。这个问题的解决方案是先设置单元格格式为文本,或者在读入时强制将手机号列转为字符串再清洗尾部的小数部分。

第三个更隐蔽的问题是重复数据。客户给的Excel里可能存在同一个客户被录了两次,原因可能是录入时名称少写了一个字或者加了空格。我在导入逻辑里增加了"公司名称去空格后完全匹配"的强去重,同时还做了一个"公司名称相似度超过85%"的弱去重提醒,让用户自己确认是否为同一家公司。这套机制上线后帮客户清理出了三百多条重复数据。

5.2 并发更新导致的数据丢失

系统上线后的第二周,一个销售主管反馈了一个问题:两个销售同时保存同一个客户的跟进记录,后保存的人把先保存的人的内容覆盖掉了。

还原这个问题的场景是这样的:销售A打开了一个客户详情页,并在跟进记录里写了一条备注;与此同时,销售B打开同一个客户详情页(可能是之前共享的收藏链接),也写了一条备注。A先点保存,B在A之后也点保存,由于B保存时提交的数据是基于他打开页面那一刻的数据快照,这个快照里并没有A的备注,因此B的保存操作直接把A的新增内容覆盖掉了。

这个问题在并发控制里叫"丢失更新"。解决方案通常有两种:悲观锁和乐观锁。对于CRM这种读多写少的系统,悲观锁显然太重了,频繁的锁等待会严重影响使用体验,所以我用了乐观锁方案。具体做法是:在模型中增加一个version整数字段,每次保存时检查数据库中的version是否和当前操作开始时读到的version一致,一致才允许更新,同时把version加一;不一致则说明数据在操作期间被别人改过了,系统抛出一个提示,让用户刷新页面后再操作。

这个方案上线后,类似的数据覆盖问题就再也没有出现过。而且我在前端也加了配合:保存按钮点击后立即禁用,防止用户重复提交;页面在长时间未操作后自动刷新数据,确保用户看到的数据始终是最新的。

5.3 大数据量下客户列表的查询性能优化

客户数量到了三千多以后,列表页的响应速度开始明显变慢了。从点击进入到页面加载完成,有时要等两秒多,这在内部系统里是用户根本不能忍的体验。

我排查了一下,发现主要问题出在列表页的查询逻辑上。开发初期为了方便,直接在客户表上做了几个外键关联查询,比如要关联显示归属销售的名字、最近一条跟进记录的时间、当前商机的总金额,这些关联查询会导致数据库做大量的多表扫描,三千条数据可能产生上万次的临时表操作。

优化的第一步是在频繁查询的字段上添加合适的索引。owner_id(归属销售)、level(客户等级)、source(客户来源)、next_follow_date(下次跟进日期)这几个字段都分别建立了单列索引,并在owner_id + level这个组合上建了联合索引。索引不是越多越好,因为维护索引也有成本,所以只针对最核心的查询路径建了必要的索引。

优化的第二步是引入Redis缓存。列表页的查询结果在五分钟内是相对稳定的,我加了一层缓存,缓存命中时直接返回序列化好的JSON数据,不再打数据库。实测优化后列表页的响应时间从两秒多降到了三百毫秒以内,基本和内存操作没有区别了。

优化的第三步是前端的分页策略。原来一页展示五十条数据并附带大量的汇总统计,后来我把统计信息单独用一个接口提供,并且加了条件缓存——只有筛选条件变化时才请求。列表数据采用懒加载和虚拟滚动,虽然三千条数据总量不算大,但这种优化策略让整个列表的交互操作都变得非常流畅。

5.4 角色权限改动不生效的怪异故障

还有一个问题花了我很长时间排查:系统管理员修改了某个销售的权限之后,那个销售反馈说"好像和没改一样",新权限完全不生效。这个现象看着很像是缓存问题,但实际上问题出在Django权限系统的某个底层机制上。

Django的PermissionRequiredMixin在每次请求时会从数据库读取权限记录,正常来说权限改动是即时生效的。但我使用的是Django的AuthenticationMiddleware提供的用户会话对象,这个对象在每个用户登录时会把权限快照缓存到session里,权限改动后,如果session没有失效,用户权限快照也一直不会刷新,这就会造成权限一直没生效的假象。

解决方案有两种。一种是在修改权限后强制清除对应用户的session(在Django的Session模型里删除记录);另一种是在用户的权限检查逻辑里每次都重新查询数据库,绕过session缓存。考虑到这个项目权限变动并不频繁,我采用了第一种方案,在管理员的权限变更页面加了一个按钮,点击之后强制清理相关用户的session并让其重新登录。后来我还扩展了一个功能:管理员修改权限时,可以勾选一个"立即生效"选项,勾选后系统自动清除对应用户的session缓存,从根源上消除了这种"改了没效果"的困惑。

6. 对DeskcommCRM后续迭代的一点思考

DeskcommCRM这个项目做到现在,核心目标已经实现了。但我在交付后的维护过程中也意识到,一个内部系统如果想发挥出更大的价值,后续的迭代方向很重要。我个人觉得可以从两个方向去扩展:一个是智能化的数据分析和提醒,比如基于历史数据预测客户流失风险、预测本月可能达成的销售额,这些东西在业务管理上的作用会非常直接;另一个是增强客户端的自助服务能力,比如让客户通过微信小程序自助提交报修申请、查看维修进度,这样既能减轻客服和销售的重复沟通负担,也能让客户感受到更专业的服务体验。

还有一个方向目前在计划中:把销售过程的数据更多维度地沉淀出来,形成公司自己的销售方法论。比如打通历史合同数据和设备维保数据,分析哪些行业客户的续约率高、哪些客户行业容易出大单、设备的哪些故障是可以通过日常保养避免的。这些分析结果如果能反哺到销售日常的动作里去,价值会超过单纯的数据展示。

写到这里,我对DeskcommCRM这个项目最大的感受是:一个内部系统的成败,七分在业务梳理,三分在技术开发。技术上的问题和方案都能找到成熟的答案,难的是能不能真正吃透业务,把业务流程里那些模糊不清、模棱两可的地方,用清晰的表和字段定义下来。如果说有什么经验可以分享给准备做类似项目的人,那一定是这句话:动手写代码之前,多花时间去听业务人员的抱怨,把他们每天工作里最头疼的事情记下来,然后一条一条去解决,这个系统的价值自然就出来了。

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

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

立即咨询