自研CRM实战:从客户全景视图到通信留痕的架构与设计

📅 发布时间:2026/10/19 16:47:20
自研CRM实战:从客户全景视图到通信留痕的架构与设计
不少做销售团队的朋友都问过我同一个问题市面上CRM一大堆免费的、付费的、云端部署的、私有化定制的为什么还要自己折腾一套“DeskcommCRM”这样的系统答案其实很简单别人做的是标准品但我们团队要的是合身的衣服。提到CRM大家第一反应是“管客户”。但真正用过的人会告诉你管客户只是表面背后是不同行业、不同团队、不同业务流程的“适配战争”。过去两年我们试过好几套通用CRM业务部门永远在提需求销售说录入太麻烦客服说客户信息不同步主管说数据报表隔天才能看。到最后CRM沦落成“把客户名单填进去存着”的电子表格这完全违背了CRM的初衷。这个项目“DeskcommCRM”就是我为了把客户管理从“记账”变成“经营”而做的自研系统。它解决的问题很具体一是让每一次与客户的互动都有迹可循二是让每个业务角色用最少的点击完成工作三是让管理层看到的永远是最新最准的数据。如果你也在考虑自研CRM或者正在挑选CRM但始终犹豫这篇博文里的设计思路、踩坑记录和关键代码思路应该能帮你省掉不少弯路。1. 为什么我不再直接用现成CRM而是自研了DeskcommCRM先说说我决定自研的导火索。当时我们团队四十多人一半销售一半客服。用了将近一年的商业CRM问题越来越明显不是软件不好而是流程不匹配。那种感觉就像买了一套精装房隔断墙都是按别人家的生活习惯砌的我们只能凑合住。1.1 旧方案的三座大山信息割裂、操作繁琐、数据不可用第一座大山是信息割裂。销售在CRM里录入客户资料客服在另一个工单系统里处理售后问题通话记录又在电话交换机里存着。三个系统互不相通结果就是销售打电话给客户之前根本不知道这个客户上周刚投诉过物流。客户体验差内部协作也累每天光对齐信息就要花不少时间。第二座大山是操作繁琐。通用CRM为了覆盖各种行业表单字段特别多新建一个客户要填二十多个必填项。销售在外面跑一天回家还要对着电脑补录入日子长了就应付了事信息质量越来越差。系统里大量客户资料停留在一两年前连联系电话号都没更新。第三座大山是数据不可用。报表是每天凌晨定时跑批的主管看到的永远是昨天的数据。再想做一些定制分析比如“按区域、按产品线、按客户星级交叉看回款趋势”后台设置又复杂最后只能把明细导出来用Excel手动做。1.2 DeskcommCRM要解决的核心需求客户全景视图、通信留痕、团队协作、数据驾驶舱在动工之前我拉上销售主管、客服主管和一线销售代表聊了好几轮最后把需求收敛成四个核心词。第一个是客户全景视图。我们要的不是一堆字段而是一个客户从首次线索到成交到售后的完整时间轴谁录入的、什么时候跟进过、聊了什么、发过什么资料、有没有下单、有没有售后问题。任何一个人打开这个客户页面三秒钟内就能知道全部来龙去脉。第二个是通信留痕。销售和客户的沟通主要靠电话。系统需要和电话交换机对接自动记录每一通电话的时间、时长、通话录音并且自动关联到对应客户。这一条能省掉销售大量手动记录的工作也为后续做话术复盘提供素材。第三个是团队协作。销售之间可以互相转交客户主管能查看团队跟进情况客服能把客户问题和对应的销售线索串起来避免“销售不知道客户已经投诉过”的尴尬。第四个是数据驾驶舱。管理层要实时看到线索量、跟进次数、转化率、回款金额这些核心指标。不需要Excel打开系统就能看还要支持按时间、按人员、按来源下钻。2. DeskcommCRM整体架构与设计思路需求清楚了接下来就是技术选型与架构设计。我没打算做重架构本质上这是一个支撑几十人团队的业务系统高并发不是首要矛盾重点是开发效率、后续可维护性以及信创环境下的可替代性。2.1 技术选型为什么选这套组合后端我选了Python的FastAPI框架前端用Vue3 Element Plus数据库用PostgreSQL缓存用Redis部署直接用Docker Compose。有些人可能觉得后端用Java更保险但对我们这种业务密集型系统FastAPI的优势很明显。它基于类型注解自动生成API文档前端对接接口时不用反复问字段含义异步原生支持让长轮询、WebSocket实时推送都很好写再加上我们是销售团队Python写数据清洗脚本、统计报表生态里的pandas、openpyxl用起来非常顺手。数据库选PostgreSQL而不是MySQL主要看中三点。一是JSONB类型客户的自定义属性可以灵活存储不用频繁改表结构二是自带行列级锁和MVCC支撑几十人的并发操作绰绰有余三是强大的全文检索和窗口函数后面做数据分析和搜索很方便。前端之所以选Vue3 Element Plus是因为团队里前端水平以“能改”为主Vue的上手成本低Element Plus组件库覆盖了大量后台管理页面的常用交互表格、表单、弹窗、日期选择器一个合格的后端工程师也能在半个小时内把页面搭起来。这里要说明一下这套技术栈适合业务逻辑重、并发量中等、需要快速迭代的内部系统。如果是面向C端的高并发场景架构思路就要完全重来。2.2 数据模型设计客户、联系人与跟进记录的核心关系整个系统的数据模型围绕三个核心实体展开客户Customer、联系人Contact、跟进记录Activity。客户表存的是公司级信息比如客户名称、所属行业、客户星级S/A/B/C、来源渠道广告、转介绍、展会等、状态线索、潜在、成交、流失。联系人表挂在客户下面一个客户可以有多个联系人每个联系人独立存姓名、职位、电话、微信、邮箱等。跟进记录表是整张时间轴的基础任何一次电话、见面、微信沟通、邮件往来都往这张表里写一条记录并关联负责人、关联客户、关联联系人、关联待办任务。核心表结构大致如下-- 客户主表 CREATE TABLE customer ( id BIGSERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL, industry VARCHAR(100), star_level CHAR(1) DEFAULT B, source VARCHAR(50), status VARCHAR(20) DEFAULT lead, owner_id BIGINT REFERENCES sys_user(id), created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); -- 联系人表 CREATE TABLE contact ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT REFERENCES customer(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, position VARCHAR(100), phone VARCHAR(30), wechat VARCHAR(100), email VARCHAR(100) ); -- 跟进记录表 CREATE TABLE activity ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT REFERENCES customer(id) ON DELETE CASCADE, contact_id BIGINT REFERENCES contact(id), owner_id BIGINT REFERENCES sys_user(id), activity_type VARCHAR(30), -- phone_call / visit / wechat / email content TEXT, next_follow_date DATE, created_at TIMESTAMPTZ DEFAULT now() );这个结构看起来简单但后面所有功能都是围绕它展开的。客户360度视图页就是把这三张表的数据串起来按时间倒序展示跟进记录和通话记录主管看板就是把这三张表按维度聚合统计。建模时最忌讳一上来就设计二十多张关联表先从最小闭环开始后面按需求添加反而更稳。2.3 权限模型与多角色支持权限模型我直接采用了成熟的RBAC基于角色的访问控制方案没有自己发明轮子。系统里有三个内置角色销售、销售主管、管理员。销售只能看到自己名下的客户主管能看到自己团队所有成员的客户管理员拥有全部权限包括系统配置、用户管理、数据导出等。在实现上我把数据权限限定做了两层。第一层是功能权限控制某个角色“能不能访问某个菜单”。第二层是数据范围权限控制“能访问哪些数据”。数据范围用几个枚举值标识本人、本部门、全部。查询时在SQL里自动拼接条件。比如销售用户在查询客户列表时系统自动加一个 owner_id 当前用户ID 的过滤条件避免在代码里到处判断角色导致漏网之鱼。这条设计后来在审计时帮了大忙。对比之前用商业CRM时权限被某个超管账号一杆子捅到底现在什么时候谁看了哪个客户的资料全部有迹可循。3. DeskcommCRM核心功能模块拆解架构和数据模型确定之后最有意思的部分就是各个功能模块的落地。这里挑几个重点模块讲一下具体的实现思路和遇到的坑。3.1 客户与联系人管理从录入到360度画像客户录入这个环节我们重点攻克了“录入难”的问题。第一版我也学通用CRM做了一大堆必填字段。结果上线三天就被销售骂了新增一个客户要填公司名、统一社会信用代码、法人、行业、规模、详细地址……光表单加载就要好几秒。后来我狠下心把必填字段砍到了三个客户名称、手机号或座机号、来源渠道。其他全部改为非必填可以等后续跟进中慢慢补齐。录入体验立刻改善销售新增客户的意愿明显提升。客户列表页做成了高聚合的“卡片列表”模式每一行显示客户名称、星级、状态、所属行业、最近跟进时间、下次跟进日期。表格支持按状态筛选、按星级排序、按关键词搜索跟进的销售甚至可以利用快捷筛选看“今天需要跟进的客户”和“超过7天没跟进的客户”不需要主管催自己就把优先级排出来了。360度客户画像页是整个系统使用频率最高的页面。页面顶部是客户的基本信息卡片底下分Tab展示跟进记录、联系人列表、通话记录、订单记录、售后工单。这里的关键点是所有数据都要在一秒内加载出来。我在后台做了聚合查询的接口一次联表查出所有Tab的数据前端切Tab时不需要再发请求体验非常流畅。3.2 跟进记录与任务提醒销售过程管理的关键很多CRM把跟进记录做成了“流水账”销售每次写一大堆内容管理者根本不会看。这套系统里我换了一个思路跟进记录不仅仅是记录更是驱动下一步行动的引擎。每条跟进记录除了填写沟通内容和方式电话/见面/微信还要设置“下次跟进日期”。保存之后系统自动在待办列表里生成一条任务提醒。到了第二天工作台首页会显示今天应跟进的客户列表点击名单直接就跳转到客户画像页省去搜索的时间。为了降低记录的抗拒感我在手机上做了“快捷记录”入口销售刚挂电话随时可以录一句要点。比如“王总说下周二才开会定供应商到时候再联系”。系统自动把这句话存为该客户的最近跟进记录并顺延下次跟进日期。这里还藏了一个用心的小设计跟进次数会被纳入数据看板作为销售活跃度的关键指标。但不看绝对次数而是看“有效跟进率”——即具备下一步动作的跟进记录占比。销售不会为了刷数据而乱录了因为录了没有设置下次动作照样不算有效。3.3 通信留痕与智能弹屏Deskcomm的“Comm”到底做了什么项目名里带“Comm”本质就落在通信集成上。我们的办公室用的是自建的FreeSWITCH电话交换机销售通过IP话机或者软电话外呼。过去通话记录分散在交换机后台销售还得自己记“今天给谁打了电话”特别费劲。DeskcommCRM和电话交换机对接之后变成了一套“点击拨号通话自动记录”的方案。销售在系统里点客户旁边的电话图标系统通过接口通知话机拨号同时创建一个“通话中”的临时记录。通话结束时交换机会通过WebSocket回调一条通话状态消息给系统包括双方号码、开始时间、结束时间、通话时长如果配置了录音还会带上录音文件的URL。系统拿到消息之后做的事比较关键通过被叫号码倒查客户和联系人找到该号码关联的客户在客户时间轴里自动生成一条通话记录显示“已通过电话通话时长4分32秒”给销售弹出一个快捷记录框提示“是否要为本次通话补充内容”。销售只要不点取消这条通话记录就算入账。这样既保证了信息完整又没给销售增加额外负担。实际上线之后大家反馈录音回放这个功能的实用价值最高。新员工不知道怎么跟客户开口翻出老销售的优秀通话录音听一听比看十篇培训文档都管用。3.4 数据看板与实时统计管理层最喜欢的功能数据看板最早是主管的需求后来做着做着我发现销售自己也爱看。整体看板页面包含几个核心卡片今日新增线索数、今日跟进次数、今日通话总时长、当前客户总量、本月成交金额。下面是几个趋势图近7天新增线索趋势、近30天转化漏斗线索-意向-报价-成交-回款以及销售个人业绩排行榜。这些指标的数据从哪来全靠实时SQL聚合不引入单独的大数据组件。因为数据量级几千到几万条PostgreSQL的窗口函数和物化视图完全能扛住。为了减少频繁聚合对主库的压力我在Redis里做了5分钟的短缓存。也就是说最坏情况下页面数据延迟5分钟但正常使用几乎感觉不出来主管也不需要等隔天的日报了。一个比较有用的视图是主管的“团队跟进制衡图”。横轴是团队成员纵轴是有效跟进次数。主管每天早会前扫一眼马上就知道前一天谁在干活、谁在摸鱼。这不是为了监控而监控而是给管理提供数据依据让那些认真跟进客户的销售得到应有的认可。4. 从零到一DeskcommCRM关键环节实现理论说了一堆进入实际操作环节。这里我会详细拆解三个关键的实现本地环境搭建、Excel客户批量导入、以及权限控制的代码落点。4.1 用Docker Compose快速搭建开发环境开发环境我直接用Docker Compose统一管理中间件后端和前端代码挂载到容器里修改即时生效。version: 3.8 services: postgres: image: postgres:15 container_name: deskcomm_postgres environment: POSTGRES_USER: deskcomm POSTGRES_PASSWORD: deskcomm123 POSTGRES_DB: deskcomm ports: - 5432:5432 volumes: - pgdata:/var/lib/postgresql/data redis: image: redis:7 container_name: deskcomm_redis ports: - 6379:6379 backend: build: ./backend container_name: deskcomm_backend command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload ports: - 8000:8000 volumes: - ./backend:/app environment: DATABASE_URL: postgresql://deskcomm:deskcomm123postgres:5432/deskcomm REDIS_URL: redis://redis:6379/0 depends_on: - postgres - redis frontend: build: ./frontend container_name: deskcomm_frontend command: npm run dev ports: - 5173:5173 volumes: - ./frontend:/app depends_on: - backend volumes: pgdata:注意几个细节。后端容器里通过环境变量读取数据库连接串而不是写死在代码里这样换环境不用改代码。数据库容器挂了数据卷重启后数据不丢。开发环境真正跑起来只需要一条docker-compose up。4.2 客户Excel批量导入pandas 批量写入的实现很多团队在从Excel往系统里导入数据时都吃过亏。几百行数据好办但如果一次性往里灌五千行很容易出现超时或者内存爆炸。我用的方案是把导入拆成三部分解析、清洗、写入。解析用Python自带的openpyxl库直接读取Excel工作簿。清洗用pandas DataFrame处理把Excel里的列名映射到系统字段名比如把“公司名称”列改写为 name把“手机号”改写为 phone。清洗时顺便做类型转换和去重手机号统一转成字符串去空格去横线同一手机号在表里出现多行的默认保留第一行并把重复行汇总报告给用户。清洗完成之后写入数据库没有用ORM一条条循环引入因为五千条循环要近一分钟。这里用psycopg2的execute_values批量写入5000行数据实测只需要两三秒。代码如下import psycopg2 from psycopg2.extras import execute_values conn psycopg2.connect(DSN) cursor conn.cursor() data df[[name, industry, star_level, source, owner_id]].values.tolist() sql INSERT INTO customer (name, industry, star_level, source, owner_id, created_at, updated_at) VALUES %s execute_values(cursor, sql, data, page_size1000) conn.commit()我自己踩过一个坑导入时没有事务包裹中途某一行数据有问题前面导入的行也被提交了结果产生了大量半截子数据。后来改成先全部导入临时表校验通过后再复制到主表。如果中途出错临时表直接清空不影响线上数据。这个稳妥的做法虽然多了一步但对数据安全很有必要。4.3 权限控制的代码落点SQL层面过滤数据范围权限控制如果只在菜单上做隐藏数据仍然可能出现越权访问。我曾经见过一些系统销售猜出URL参数就看到了其他团队的客户。所以在DeskcommCRM里权限控制的关键落点在数据库查询层。用一个统一的函数处理数据范围过滤from sqlalchemy import or_ def get_scoped_query(model, user): 根据用户角色返回过滤后的查询条件 if user.role admin: return model.id.in_(select(model.id)) if user.role manager: team_user_ids get_team_user_ids(user) return model.owner_id.in_(team_user_ids) # 普通销售只能看到自己的 return model.owner_id user.id所有涉及客户列表、联系人、跟进记录的查询都强制走这个函数拼接条件。接口层不直接信任前端传的任何ID参数一律以当前登录用户的权限范围为准。还有一点容易被忽略删除和编辑操作也需要同样的权限校验。用户提交修改请求的时候后端要再次检查这个客户是否属于当前用户的数据范围不能只在前端界面上隐藏按钮。我见过有些系统只在界面上控制数据接口暴露了Update接口结果被有心人利用。5. 上线之后我踩过的坑和排查实录系统上线后并不是万事大吉反而最锻炼人的阶段才开始。这里整理三个很典型的线上问题和排查过程。5.1 同名称客户合并导致的关联数据错乱第一个坑出现在导入老客户资料的时候。当时表里已经有了一家“华信科技有限公司”导入的新Excel里也有一家“华信科技有限公司”。系统按“同名即同一家客户”的逻辑直接把Excel里的数据合并进了已有客户。问题来了Excel里的联系人“张三”已经有手机号而系统里此前已经录过“张三”同一家公司但不同手机号的记录。合并后系统生成了两个张三并且后续关联的通话记录一会儿挂在旧张三下面一会儿挂到新张三下面客户历史时间轴直接乱了套。排查过程花了一天。先是梳理了客户合并时的关联关系把所有历史记录统一迁移到主联系人ID下然后修改了合并逻辑不再简单按客户名称合并而是要求“客户名称 联系人手机号”同时匹配才算重复否则一律新建客户。这个改动的代价是导入速度慢了一点点但对数据准确性的提升是决定性的。现在导入时还会预先弹一个新客户与已有客户的相似名单让导入者确认是“继续新建”还是“合并到已有客户”把选择权交给业务人员系统不替他们做决定。5.2 通话记录重复入库第二个坑发生在我做通信对接测试时。FreeSWITCH的回调机制在偶发情况下会重复推送同一条通话记录。原本设计的是“按通话唯一ID去重”但我当时偷懒没有加唯一约束结果后台出现了一堆完全重复的通话记录。解决方式在数据库层级加了一个唯一索引CREATE UNIQUE INDEX idx_call_unique ON call_log (external_call_id);外部通话ID在交换机推送的消息里是唯一的。回调和手动补偿推送即使重复数据库也会拒绝插入重复记录应用层不需要额外写判断。这个教训告诉我凡是外部接口推送的数据一定要在落库时做幂等处理否则上游一抖动下游就开始乱套。5.3 Excel导出与大数据量下的内存优化第三个坑是导出客户列表时的内存问题。业务部门特别喜欢用筛选条件导出客户明细尤其是“导出所有华东地区的S级客户”这类操作不小心就触发上百万行的数据。代码早期直接to_excel会把所有导出的数据全部加载到内存然后一次性写入文件几次大导出之后服务器内存报警其他服务也跟着遭殃。后来改成分页流式查询用游标一条条读取数据每凑齐5000行就写一批到Excel文件里写完一批释放一批内存。import pandas as pd from sqlalchemy import text def export_large_query(sql, filename): offset 0 batch_size 5000 first True while True: rows db.execute(text(sql).bindparams(offsetoffset, limitbatch_size)).fetchall() if not rows: break df pd.DataFrame(rows) if first: df.to_excel(filename, indexFalse) first False else: with pd.ExcelWriter(filename, modea, engineopenpyxl, if_sheet_existsoverlay) as writer: df.to_excel(writer, startrowwriter.sheets[Sheet1].max_row, indexFalse, headerFalse) offset batch_size实际用下来三百万行数据导出时间从原来的二十多分钟降到了七分钟内存占用也稳在了一个较低的范围内。这里要注意分批写Excel时用if_sheet_existsoverlay才能追加数据不然会反复覆盖第一个Sheet。6. 使用DeskcommCRM一年后的一些沉淀系统上线已经一年多要说最大的收获不是代码量本身而是我搞明白了一个道理好的CRM不是在软件层面做到多强大而是要让业务人员觉得“这个系统好用我愿意用用它让我的工作变简单了。”DeskcommCRM在功能之外我还沉淀了几条经验写下来给想自研工具的朋友参考。第一先做减法再做加法。第一个版本就是客户管理、跟进记录、任务提醒这三大块。那些花哨的AI预测、客户画像标签、批量邮件营销等基础模块用顺手了再往上面加。一上来就奔着大而全大概率半年都上不了线。第二把业务人员当成产品的共同设计者。销售代表提出的“挂电话后自动弹屏记录”这个需求比我坐在办公室里想的任何功能都更能提升系统价值。每周拉一个业务人员坐着看他们操作比收集一堆反馈工单要高效得多。第三数据准确性比功能丰富更重要。客户数据乱了后面做任何分析都是误导决策。为了保障数据质量多花时间在导入校验、去重合并、权限隔离这些“内功”上比多开发几个花哨页面有价值得多。