1. 项目背景:为什么放着现成CRM不用,非要搞一个 DeskcommCRM
先交代一下背景。我之前所在的团队做的是B端业务,客户从线索到成交再到售后,中间牵扯电话、邮件、企业微信、自有IM、线下拜访记录,信息散落得一塌糊涂。销售在Excel里记跟进,客服在工单系统里看问题,售后在聊天记录里找上下文,同一个客户今天被销售打电话推销,明天又被客服打电话回访,客户体验差不说,内部怨气也大。市面上主流的几套CRM要么偏重销售漏斗、要么偏重工单流程,但我们真正缺的,是一个能把“所有沟通记录”串起来的东西。
后来我们内部决定自己搞一个轻量级CRM,代号DeskcommCRM。Desk就是桌面工位,comm是communication,说白了就是“坐在工位上的沟通管理”。这个定位从一开始就很具体:我们不要大而全的数字化经营平台,只解决两件事——所有客户沟通留痕,所有跟进动作有提醒。项目从立项到内测花了大概三个月,之后陆续有周边团队接入,到现在稳定跑了将近两年。这篇就是把这个项目的设计思路、踩坑过程、实操细节完整复盘一遍,给同样被沟通碎片化折磨的团队做一个参考。
这个系统适合什么样的团队?我的判断是:20到200人的销售+客服团队,业务以沟通驱动为主,客户量在几千到几万级别,希望用较低成本把“无记录沟通”变成“有记录资产”。如果你已经在用大型CRM且用得挺好,那DeskcommCRM对你没太大意义;但如果你每次开会都在互相问“那个客户上次聊到哪了”,这篇文章应该能帮到你。
2. 整体设计:把“沟通记录”当成系统的第一公民
2.1 从通讯倒推数据模型
大部分CRM的设计起点是“客户”或者“销售机会”,先有客户档案,再往上挂各种业务数据。DeskcommCRM反过来,我们的起点是“一次沟通”。
为什么会这样设计?因为实际业务里,一个客户的信息往往是先从某次电话、某封邮件、某条消息里冒出来的,然后你才去新建客户档案。如果系统强制先建档再记录,销售就会觉得麻烦,结果就是拒绝用系统。所以我们把“沟通记录”作为原子数据,客户、联系人、商机这些都可以从沟通记录里反向聚合出来。这个看起来不起眼的选择,是整个项目后面所有设计能走通的前提。
在具体落地时,我们把一次沟通拆成几个要素:时间、渠道、发起方向、参与人、关联客户、关联工单、内容正文、标签。渠道包括来电、去电、邮件、会话、面访。发起方向只有两个值,inbound还是outbound,别说“转接”“转发”这些复杂状态,免得模型一上来就失控。标签是运营后来强烈要求的,一开始我抵触,觉得标签会被乱用,但实践证明标签对检索帮助极大,只要限制单条记录最多打三个标签就行。
核心表结构其实就三张:customers、contacts、communications。很多系统会再加一张activities表把任务和事件都塞进去,我们后来也加了,但是独立出来的,因为“待办跟进”和“已完成沟通”的生命周期完全不同,混在一起查询会越来越慢、逻辑也会越来越绕。
2.2 选型:为什么没有直接买现成CRM
必须承认,当时评估过市面上的开源CRM,也试用过几套商用SaaS。放弃的原因不是它们功能不行,而是我们业务里有一个很难绕开的点:商机阶段和工单状态之间会互相联动。
举个例子,客户报了一个故障工单,如果这个工单属于“严重影响使用”的级别,那么关联的商机阶段要自动冻结;工程师在工单里写了“客户可能因此流失”的备注,系统要自动给客户成功经理推送一条提醒。这种业务规则在通用CRM里不是不能配,但要配到我们想要的灵活度,要么加钱,要么加开发量,算下来并不比自研省多少。
另外还有一个务实原因:当时的系统集成需求很杂——企业微信会话存档、自建呼叫中心、邮件代收代发,这些接口文档经常变,用商用系统未必能保证当天就能适配。自己掌握通讯层的接入,出了问题可以在一天内修掉,这个响应速度在业务侧看来是实打实的价值。
所以选型结论很简单:买现成的省事,但省不了事的时候,自己干反而可控。DeskcommCRM采用了前后端分离结构,后端用Python FastAPI,前端用React,数据库选了PostgreSQL。为什么是现代技术栈?因为团队里有人熟,好招人,生态好,性能对我们这个量级绰绰有余。
2.3 模块划分与权限模型
模块划分上,我们没有按传统CRM那样拆成客户、商机、工单三大模块。DeskcommCRM只拆成五个工作区:工作台、沟通记录、客户管理、任务中心、数据报表。
工作台是一天所有跟进提醒的集合,销售和客服打开系统第一眼看到的就是“今天需要处理的沟通任务”。沟通记录是全量留痕的日志流,可以按客户、按渠道、按人群检索。客户管理不再是核心,反而像一个聚合视图,点进一个客户,能看到所有历史沟通和所有待办。任务中心是手动创建的提醒和系统自动派发的联动任务。数据报表用来支撑管理层日常看板,这个模块我们做得比较克制,后面细说。
权限模型是项目里反复调整最多的地方。一开始我们只分管理员和普通用户,结果管理员的“全量可见”导致客服能看销售报价,销售能看客服内部吐槽,一个月内出过两次事故。后来改成了四层:系统管理员、团队主管、坐席、访客。核心原则是“数据按团队隔离,跨团队默认不可见,需要协作时通过共享链接临时授权”。共享链接我们当时做得比较简陋,到期时间、访问密码都有,但没有做审计日志,这是后来才补上的,这块也确实应该在第一版就做进去。
3. 实操搭建:从零把 DeskcommCRM 跑起来的关键环节
3.1 环境准备与基础骨架
先说技术栈的选择逻辑。后端用FastAPI主要是因为异步支持好,呼叫中心回调和WebSocket消息推送对异步有硬性需求。前端用React是因为生态成熟,表格组件、富文本、拖拽都容易找到现成方案。数据库用PostgreSQL,一听就很“标配”,但实际操作里PostgreSQL的JSONB、全文检索、行级安全策略,都帮我们省了不少事。
代码仓库的结构是这样划分的:
deskcomm-crm/ ├── backend/ │ ├── app/ │ │ ├── api/ # 路由层 │ │ ├── models/ # SQLAlchemy 模型 │ │ ├── schemas/ # Pydantic 校验 │ │ ├── services/ # 业务逻辑 │ │ ├── integrations/ # 呼叫中心、邮件、企微等外部接入 │ │ └── worker/ # 异步任务 │ ├── migrations/ # Alembic 迁移脚本 │ └── tests/ ├── frontend/ │ ├── src/ │ │ ├── pages/ │ │ ├── components/ │ │ ├── stores/ # 状态管理 │ │ └── api/ # 接口封装 └── docker-compose.yml建议第一次搭建时直接把Docker编排文件配好,PostgreSQL、Redis、后端、前端四个容器一启动,开发环境和生产环境差异会小很多。我们前两周没用容器,结果一到联调阶段每个人本地环境都不一样,白白浪费了好几天。
3.2 三张核心表的建表逻辑与字段设计
下面这段是DeskcommCRM的关键资产,如果你也想搭类似的系统,可以直接参考,但字符长度和字段增减要按自己业务调整。
customers 表的关键点:
CREATE TABLE customers ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, level SMALLINT NOT NULL DEFAULT 3, status VARCHAR(32) NOT NULL DEFAULT 'active', owner_id BIGINT, team_id BIGINT NOT NULL, source VARCHAR(64), unified_tag TEXT[], created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );不要一上来就把客户表搞出几十个字段,什么行业、规模、地区、信用等级全放进去,结果录入时一堆留空,查询时一堆无效条件。我们的经验是基础字段控制在10个以内,其他信息全部放到沟通记录和便签里,靠全文检索去捞。客户等级用1到5的整数而不是“A类B类”,因为整数支持大小比较,报表里可以直接算加权值。
contacts 表要考虑“一个客户多联系人”和“一个联系人跨多个客户”两种情况。我们一开始只做了前者,后来发现采购对接人从A公司跳槽到B公司,客户关系就断了,才改成多对多。这里有一个很值得说的字段是preferred_channel,也就是联系人偏好渠道——有人喜欢微信,有人只回邮件,这个字段直接在卡片页用大图标展示,因为一线坐席看到这个就能少踩很多沟通雷。
communications 表是承载最多的表:
CREATE TABLE communications ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT, contact_id BIGINT, channel VARCHAR(16) NOT NULL, direction VARCHAR(8) NOT NULL, subject VARCHAR(500), content TEXT, happened_at TIMESTAMPTZ NOT NULL, related_ticket_id BIGINT, related_opportunity_id BIGINT, tags TEXT[], created_by BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );这里有个实操细节:每条沟通记录必须有created_by和happened_at,两者分开。created_by是这条记录由谁创建,happened_at是沟通实际发生的时间。因为经常有这种情况——客服晚上7点接到电话,第二天早上9点才补记录,如果只有一个字段,数据要么不准、要么不可信。
索引方面,最常用查询是“某个客户的全部沟通记录”,所以customer_id + happened_at的联合索引必须建。其次是按时间和渠道筛选的列表页,建一个channel + happened_at的联合索引。人员量上来之后会发现全文检索很吃资源,建议直接用PostgreSQL内置的tsvector字段,数据量在百万级以内完全够用,不需要单独引搜索引擎。
3.3 通讯渠道接入:把电话、邮件、企微汇成一条流
这部分是整个项目里最琐碎、最需要耐心的环节。我们接了三类渠道,每一类的接入思路都不一样。
电话渠道走的是呼叫中心回调方案。坐席用呼叫中心软电话,通话结束时呼叫中心会向我们的回调地址推一条话单,里面有通话ID、主叫、被叫、时长、录音URL。我们在回调里根据被叫号码反查客户,查不到就自动建一个“未分配客户”的占位档案,等坐席在系统里补充信息。这一块最常见的问题是被叫号码格式不统一,手机号有+86前缀、有没前缀,座机有分机号,所以入参时统一做归一化处理,正则规则一定要和通信运营商确认清楚。
邮件渠道用的是IMAP收信加SMTP代发。团队里之前没有邮件服务器,我们直接在代码里集成了一个邮件发送函数,发件人统一走一个备用的SMTP域名。IMAP收信则按一个独立邮箱账号去拉取,拉下来后解析From、To、Subject、正文,然后根据正文里的客户编号识别归属。这个编号规则是整个接入方案里最重要的约定:邮件标题统一写成“【客户ID-12345】关于xxx”,没有这个规矩,邮件和客户的关系根本对不上。
企业微信渠道其实最麻烦。会话存档需要企业管理员授权,而且只能拿到员工的聊天记录,拿不到客户侧主动删掉的消息。数据接口每天有拉取限额,必须做增量同步,用游标记录到哪一条为止。这一块的稳定性全靠重试机制兜底,消费者进程要能识别重复消息并做幂等处理,防止同一句话被存了三次。
3.4 跟进提醒:任务调度怎么做到“不过度打扰”
很多CRM的提醒功能容易被吐槽“一到点就弹窗,弹完就忘”。我们设计DeskcommCRM的任务提醒时定了三条原则:只提醒今天该做的、提醒必须有上下文、可以一键改期。
任务生成有两条来源。一是手动创建,销售或客服在某个客户页面点“添加跟进”,设置提醒时间。二是系统规则触发,比如客户报了Level 1的工单,到了48小时未解决,系统自动给客户成功经理生成一个“联系客户同步进展”的任务。这两种任务在任务中心里以不同颜色区分,但处理动作统一:完成、改期、转交。
底层调度我们用的是Celery Beat,每天凌晨会生成一次全量日任务快照,而不是实时触发。这个设计有讲究:实时触发的提醒容易在业务高峰时挤爆消息队列,而每天凌晨算一次,白天只要从Redis里取“当日提醒列表”,性能压力小很多。Redis里的key是user_id:YYYY-MM-DD,value是任务ID集合,坐席端一到当天零点就能看到新清单。
改期操作要设计得足够顺手。我们在任务卡片上直接放了三个快捷按钮:改到明天、改到下周同一时间、选择具体时间。实测下来,九成用户只用前两个,第三个反而用得少。这个细节告诉我们一个道理:不要试图把系统做成万能工具,用户要的是最少思考完成操作。
4. 从“能跑”到“好用”:流程、界面与数据迁移的落地细节
4.1 销售视角和客服视角为什么要分开
第一版做出来的时候,销售和客服用的是同一个工作台,结果两边都抱怨。销售觉得信息太吵,满屏是工单状态;客服觉得工作台偏商机管理,找不到“未响应超时”的会话。后来我们把工作台按角色拆成两套视图,底层数据完全一样,只是默认排序、快捷入口和展示字段不同。
销售工作台的默认Tab是“今日待跟进客户”,每个客户卡片显示最近一次沟通摘要、优先级标签、下次联系时间。客服工作台的默认Tab是“待处理会话”,排序规则是按“客户最后一条消息的时间”从小到大排列,超过4小时没回复的会标红。这里有一个关键判断:客户成功经理和客服其实需求不同,前者看重客户健康度,后者看重响应时效,所以后来又加了一个“客户成功”独立视图,三套视图共用一个任务中心。
这个拆分的代价是前端状态管理变复杂了。我们用的是Zustand,每个视图维护自己的store,切换Tab时不会互相污染。如果你用Redux,一定要在设计store结构时就把“角色视图”作为顶层维度,否则后面会陷入疯狂清缓存的泥潭。
4.2 历史数据迁移:Excel、旧系统和碎片信息的合并
迁移是整个项目中被低估最多的环节。我们当时从旧系统和Excel里导出了大概6万条客户记录、13万条跟进记录,听起来不多,但质量参差不齐到让人崩溃。
第一次迁移直接跑脚本导入,结果发现同一客户“北京华信科技有限公司”和“华信科技(北京)”被建成了两个客户档案,销售各管一边,数据重复率高达百分之十几。后来我们加了合并规则:企业客户按统一社会信用代码匹配,没有信用代码的按域名归一化匹配,再没有的按地址近似匹配。这套规则不是写个脚本就完,而是做成一个“合并建议列表”,由运营人工确认后再合并,只靠算法自动合并很容易把不是同一家的也合了。
迁移的另一个大坑是时间字段。旧Excel里日期格式五花八门,“2024-1-5”“2024.1.5”“2024年1月5日”并存,解析脚本写了整整一天。如果你们也要做类似迁移,我的建议是先写一个数据质量检测脚本,输出所有异常格式的样例,修完一遍再跑正式导入,别用“先导进去再说”的心态。
4.3 报表设计:宁可少,不要假
报表模块我们反复砍了好几轮。管理层一开始提了一堆报表需求,比如每个销售每天通话时长趋势、每个客户的沟通频率热力图、工单转化率漏斗、流失预警模型。如果全做,可能两三个月都在做报表。后来我们只保留了四个:今日工作负荷、跟进时效性、渠道分布、客户健康度预警。
原因是只有这四个报表的数据口径是明确且稳定的。像“通话时长趋势”这种,呼叫中心的话单和坐席补录的沟通记录两者时长经常不一致,口径对不上做出来的报表只会让管理层对系统产生不信任。这个问题非常致命——一次报表数据不准,前面十次准确也白搭。所以要把口径完全定义清楚再上线。
客户健康度预警我们用了最简单但特别有效的算法:30天内没有新增沟通记录、且存在未关闭工单的客户,自动标记为“关注对象”。不搞机器学习,因为样本量根本不够,硬上模型只会产出没人信的概率。报表的价值在决策辅助,不在数字游戏,这一点越早想明白越好。
5. 典型问题排查实录与避坑指南
5.1 工单重复:同一个问题被电话、邮件、企微报了三遍
上线一个月后,运营反馈最多的问题是重复工单。客户同一个问题,先在企微里说了一遍,又发了一封邮件,客服在电话里又接了一遍,结果系统里生成三条工单,三条工单各配一个团队跟进,客户被问了三遍同样的问题,非常尴尬。
我们采取了两层方案。第一层是入口识别,在创建工单时做相似度匹配,把标题和正文做向量化,设定一个阈值,相似度超过0.8就提示“疑似重复工单”,由坐席决定合并还是新建。这个方案最开始用简单的TF-IDF凑合,后面换成了SentenceTransformer的轻量模型,效果好了很多。第二层是规则兜底,同一个客户、同一个问题分类、48小时内创建的多条工单,自动进入“待合并”队列,由主管确认合并。
这个特性上线后,重复反馈的比例从大约12%降到了4%。关键不是算法多高级,而是入口处让坐席意识到“这条可能是重复的”,人的判断加上系统的提示才最可靠。
5.2 通信记录丢失:一次排查IBM MQ消费者线程被阻塞的惊魂24小时
DeskcommCRM上线之后遇到过最严重的一次线上事故,是某天早上客服反馈“昨天的部分电话记录不见了”。排查过程非常有代表性,值得记录。
第一步查数据库,发现确实有缺失,但不是全部渠道,只有电话渠道的一部分。第二步查呼叫中心的回调日志,发现凌晨3点到6点之间,有一批回调请求返回了500,原因是我们接口的超时时间设的是3秒,而后端业务逻辑里有一段时间外的SQL查询,导致处理时间超过了3秒。第三步查异步任务,回调接口收到消息后马上返回200,然后丢到异步队列里处理,但异步消费组的线程因为数据结构问题,处理某条脏数据时抛出了非捕获异常,整个消费者线程挂了,后面的消息全部积压在队列里。
根因是脏数据加异常处理不健壮。我们做了三个修复:回调接口优化为纯入队操作,任何业务逻辑都不放在请求线程里;消息队列消费者包了一层try-catch循环,单条消息失败就记录并跳过;队列加了一个告警监控,积压超过100条就报警。这套修复方案后来也成了DeskcommCRM处理类似问题的标准模板:入口收窄、消费健壮、监控兜底。
5.3 搜索慢、并发问题与系统卡顿的优化过程
上线半年后,客户量涨到十几万,沟通记录超过百万条,列表页越来越慢。最惨的是带全文搜索的页面,一个关键词查下去要七八秒。
优化做了三件事。第一,全文检索从LIKE改成了tsvector,PostgreSQL的GIN索引响应速度直接提升了一个量级,这个改动最小收益最大。第二,列表查询做了增量加载,只返回一个月内的默认数据,需要看更早的数据可以显式切换“历史模式”,避免每次默认查询都扫全表。第三,把热数据和冷数据分离,三个月内的沟通记录放主表,三个月以上自动归档到communication_archive表,查询时界面默认走主表。
并发方面我们没有遇到特别大的压力,峰值在线用户大概两三百,主要瓶颈反而在Redis连接池。一开始用的是默认连接池大小,早高峰一上来导致排队,后来把连接池调到128,同时把大量只读请求改成接PostgreSQL的只读副本,问题就解决了。
6. 后续演进和我的真实体会
DeskcommCRM到目前为止没有计划做成商业化产品,所有代码都跑在团队内部的基础设施上。但它的设计思路确实可以复制,尤其是“沟通记录为第一公民”“任务提醒给上下文”“报表口径先统一再开发”这几条,放到任何一个以沟通驱动业务的团队都适用。
我个人实际用下来最深的体会是,CRM系统成功与否,七成在业务侧,三成在技术侧。如果销售不信任系统数据,觉得系统是“监控工具”,再强的功能也推不下去。我们做了几件事来建立这种信任:每周发一封数据质量周报,让全员知道哪些数据有缺失、哪些字段要补;对“补录率”高的团队公开表扬而不是通报批评;系统上线前三个月保留所有手工Excel,允许用户并行维护,让他们自己对比出系统更省事,接受度自然就上来了。
另一个值得分享的扩展方向是接口开放。我们后来把DeskcommCRM的API做成了一套公开的内部接口,其他内部系统可以直接调用查询客户最近沟通记录,目前已经有三个系统接入了,包括财务部的合同审批流程和运维部的告警通知中心。这算是当初设计时没有预料到但回头来看很有价值的决定,所以如果你也要做类似系统,建议一开始就为API留好扩展空间。
最后分享一个小技巧,给所有准备做内部工具的团队:不要在大规模推广前追求功能百分百完善。先用最小可用版本跑通3个核心团队,把他们的反馈作为下一轮迭代的依据。我们最难熬的不是开发期,而是第一个月每天被催补功能、补权限、补导出的那些日子,但正是这些来自真实业务反馈的“脏需求”,让DeskcommCRM最终长成了团队真正需要的样子。