从零搭建CRM:数据模型、权限与跟进机制的实战复盘
发布时间:2026/10/19 9:13:32 锦皓数字建站

DeskcommCRM 并不是一个商业产品它是我给一家销售型小团队从零搭起来的内部客户关系管理系统。刚开始立项的时候老板的想法很简单把销售手里的客户资料从 Excel 和微信聊天记录里救出来放到一个所有人都能看得见、又不会互相乱改的地方。做下来以后我发现越是听起来简单的需求落地时越容易翻车CRM 这种系统尤其如此——因为它管的不只是数据而是人和人之间的协作节奏。这套系统最适合两类人参考一类是销售或运营团队里的技术负责人想用最小的成本解决客户跟进混乱的问题另一类是想系统搞明白 CRM 背后核心逻辑的开发者看看一个真实的客户管理工具从表结构设计、权限模型到跟进流程引擎到底是怎么一层层长出来的。项目跑起来以后客户从录入到成交的平均周期缩短了一截更重要的是销售离职交接时不再出现“人走了、客户也带走了”的烂账。先说我为什么没有直接买现成的 CRM。市面上开源的、付费的 CRM 我前后试过不下八款最后放弃的原因出奇一致要么业务字段改不动要么销售觉得录入负担太重、用不起来。DeskcommCRM 从第一天起就把“销售愿意用”当成最高优先级宁可功能少一点也要让每一次点击都有即时反馈。1. 整体设计思路为什么做一个以“跟进”为中心的 CRM1.1 核心需求拆解不是管客户是管“下一步动作”项目刚启动时我照着传统 CRM 的思路画了一堆模块客户管理、联系人管理、商机管理、合同管理、产品库、报表中心。画完以后拿给销售负责人看对方眼神里全是抗拒。后来我坐在旁边看了一个下午的销售工作过程才明白问题出在哪销售真正的工作流不是“录入客户资料”而是“今天要联系谁、上次聊到哪了、接下来做什么”。所以 DeskcommCRM 的设计主轴被我彻底换掉了。所有页面都围绕“下一步动作”来组织首页不是一堆统计图表而是一份按计划时间排序的跟进清单客户详情页的第一屏不是字段表单而是这条客户最近五次的沟通时间线和待办任务。这个改动听起来小实际影响巨大——销售打开系统的第一反应从“又要填表了”变成了“哦原来我该干这个”。我把这种方式叫“动作优先”的设计原则。数据录入不再是一堆必填字段而是嵌入在“记一笔跟进”这个动作里。每一次跟进顺手把客户的公司、联系人、阶段、金额一起更新系统会用时间戳把变更历史自动记录下来。也就是说数据是被“用”出来的而不是被“填”出来的。1.2 架构取舍单体应用就够别急着上微服务很多开发者一听到 CRM 就会想到复杂的权限、多租户、海量数据恨不得起步就是微服务。我的判断完全相反一个几十人规模的销售团队核心数据量撑死几十万行单体应用用起来最顺、最稳也最好维护。DeskcommCRM 采用的就是前后端分离的单体后端加一个 PostgreSQL 数据库部署在一台 4 核 8G 的云服务器上跑了半年CPU 日常占用基本没超过 15%。这个选择背后的逻辑很直接系统复杂度的增长点不在并发而在业务规则。真正的难点是权限边界怎么划、商机阶段怎么流转、数据怎么去重合并这些跟微服务一点关系都没有。如果一开始就拆服务光处理服务间的事务一致性和调用链路就能把项目拖垮。我把可扩展性留在了数据层数据库尽量用标准 SQL避免不必要存储过程对外提供 REST API将来真需要拆服务把跟进模块、报表模块单独抽出去边界是清晰的。但这是后话先把一个能用的东西跑起来比什么都重要。1.3 技术栈选型选自己半夜能爬起来改的技术栈我选了 Python 的 FastAPI 做后端前端用 Vue 3 加 Element Plus数据库用 PostgreSQLORM 用 SQLAlchemy。选 FastAPI 而不是 Django是因为 Django 自带的后台管理在这个场景下反而累赘FastAPI 的自动接口文档对后续对接小程序和第三方工具特别方便。前端用 Vue 是因为生态成熟Element 的表格和表单组件用来做业务系统效率极高。这里我想给个很朴素的建议选技术栈的标准不是“最流行”而是“你半夜三点被叫起来改 bug 时还能不能想起来代码写在哪”。团队里如果有更熟悉别的语言的成员完全可以换核心逻辑不受影响。DeskcommCRM 的架构核心在于业务模型跟语言绑定很弱。2. 数据模型设计这块是整个系统的地基2.1 核心表结构客户、联系人、商机怎么关联数据模型我前后改过四版最后稳定下来的结构不复杂总共七张核心表用户表、角色表、客户表、联系人表、商机表、跟进记录表、待办任务表。客户和联系人是 1 对 N 关系客户和商机是 1 对 N 关系联系人和商机会通过中间表做关联因为一个联系人可能同时出现在多个商机里。CREATE TABLE customers ( id BIGSERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT active, level VARCHAR(20) NOT NULL DEFAULT new, owner_id BIGINT NOT NULL REFERENCES users(id), source VARCHAR(50), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE follow_ups ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL REFERENCES customers(id), contact_id BIGINT REFERENCES contacts(id), deal_id BIGINT REFERENCES deals(id), user_id BIGINT NOT NULL REFERENCES users(id), action_type VARCHAR(20) NOT NULL, content TEXT, next_action VARCHAR(200), next_time TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );这里最关键的决定是把跟进记录做成了独立表而不是在客户表上盖一个“最近联系时间”的字段。原因很实际跟进记录是判断一个客户价值的最核心依据它既要支持任意时间点的回溯又要能跟商机、联系人、待办任务做关联。盖字段的做法会让历史信息全部丢失等于把系统最重要的资产扔了。每张表上都加了owner_id这是数据权限的第一道关口。另外一个容易被忽略的点是所有业务表我都加了created_at和updated_at虽然多写几个字段但排查问题时时间戳就是救命稻草。2.2 字段设计上的实战调整少用枚举多用配置表早期我在客户阶段字段上用了数据库枚举类型比如new、contacted、negotiating、won、lost后来业务说要在“新客户”和“洽谈中”之间加一个“已加微信”我不得不跑一次迁移。改了几次以后我悟了凡是业务会自己定义状态的字段一律不要用数据库枚举改成一张配置表或者前端字典维护。CREATE TABLE deal_stages ( code VARCHAR(30) PRIMARY KEY, name VARCHAR(100) NOT NULL, sort_order INT NOT NULL, is_active BOOLEAN NOT NULL DEFAULT true );这么做的好处是业务调整阶段时只需要改配置表不需要发版。代价是代码里要多做一次字典缓存。我当时用了一个很简单的方案服务启动时把配置表加载到内存提供/api/dict/stages这样的接口给前端拉取改动后重新加载即可。这个设计在后期帮了大忙。销售总监有一天说要把“赢单”拆成“赢单待回款”和“已回款”两段改配置、重新排序、刷新页面十分钟搞定全程没有动代码。2.3 索引与查询优化热数据要单独“喂饱”客户列表和跟进清单是使用频率最高的页面也是最容易出现“越用越慢”的地方。我的方案是给高频查询单独建索引并且让索引针对真实的查询模式。比如客户列表页最常见的过滤条件是“我的客户 最近跟进时间倒序”那就建一个(owner_id, updated_at DESC)的复合索引而不是盲目给每个字段都加索引。跟进记录表也一样列表页频繁按customer_id和created_at查复合索引(customer_id, created_at DESC)必不可少。建表的时候我特意测过在 50 万行数据量下有复合索引和无索引的查询耗时差了 80 倍这个差距在销售天天点开的页面上是能直接感知到的。我还做了一张专门的“客户摘要表”用触发器或者定时任务把客户的核心统计信息商机总额、最近跟进时间、下步计划提前算好存进去。这样客户列表页只需要查一张表就能拿到所有展示数据不用每次现算聚合响应时间稳定在 100ms 以内。3. 核心功能模块的落地细节3.1 权限模型数据隔离怎么做才不吵架权限是 CRM 里最容易引发团队矛盾的点。DeskcommCRM 的权限模型分两层功能权限和数据权限。功能权限走传统的 RBAC角色有管理员、销售主管、销售人员、运营人员对应菜单和按钮的增删改查权限。数据权限才是真正难搞的部分我设计了三种可见范围仅本人、本部门、全员。实现方式不复杂就是在查询层统一注入数据可见范围的条件。后端封装了一个叫ScopeFilter的模块每个查询自动附带当前用户的可见范围条件。销售主管默认能看到本部门数据但跨部门数据绝对看不到这是用硬编码条件控制的不是为了方便直接放开。这里有一点我踩过坑权限判断不能在多个接口里各写各的必须统一走一个公共层。早期我在几个接口里手工写WHERE owner_id current_user_id后来客户列表、商机列表、待办列表的权限逻辑开始出现不一致一个接口漏了条件就出现数据越权。统一封装以后整个系统的权限逻辑只有一个入口审查起来非常清晰。3.2 待办与跟进管道把“今天该联系谁”推到眼前DeskcommCRM 最受销售欢迎的功能不是客户管理而是首页的那份“今日待办”。它的逻辑很简单所有跟进记录如果有next_time并且小于当前时间就自动汇总成一个待办清单按紧急程度排序。加上 8 个小时没跟进的自动标红销售只要打开系统就知道该干嘛。待办的实现其实是一个视图加一次查询不复杂但体感极好。我特意把“完成跟进”的操作设计成在待办列表里直接展开表单填完自动刷新整个过程不到 30 秒。一个功能有没有人用往往就差在这 30 秒上。如果点进去还要跳转三五个页面才能记完销售很快就又会回到小本本上。商机管道视图我做得比较克制没有用什么花哨的拖拽看板就是一个按阶段分列的表格。每列显示商机名称、金额、预计成交时间、最近跟进人点进去看详情。柱子上的数字用聚合查询算出来展示每个阶段的商机总金额。这个视图是销售主管开会时用得最多的因为一眼能看出哪里有商机卡住不动了。3.3 客户查重与合并最容易忽略又最显价值的环节客户资料重复是 CRM 系统的慢性病。销售录入客户时不会先去搜一遍有没有重复于是“某某科技有限公司”在系统里出现三四份跟进记录散落在不同重复客户下报表看到的数据全是虚的。我在 DeskcommCRM 里做了一个“疑似重复”的检测机制录入或编辑客户名称时用相似度算法和现有客户比对超过阈值就弹提示。实现上我用的是 PostgreSQL 的pg_trgm扩展对客户名称做三元组模糊匹配再结合统一社会信用代码、官网域名做二次校验。CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX ON customers USING gin (name gin_trgm_ops); SELECT id, name, similarity(name, 某某科技) AS sim FROM customers WHERE name % 某某科技 ORDER BY sim DESC LIMIT 10;查重只是第一步更重要的是合并功能。合并时要以一个“主客户”为基准把其他客户的联系人、商机、跟进记录全部迁移过来然后删除重复客户。这个过程必须有事务包裹一旦失败要能整体回滚避免出现“跟记录过去了客户没了”的尴尬情况。我在这里写了个迁移任务每个关联表都做了移转记录方便出问题时定位。4. 实操实录从零搭建的关键步骤4.1 第一步先把主流程走通再做报表我犯过不少项目都会犯的错一上来先做客户列表和报表结果跟进行为没人买单。做 DeskcommCRM 的第一阶段我只交付了三件事能录入客户、能记跟进、能生成明日待办。这三个功能串起来就形成了一个最小闭环销售团队从第一天就能用起来而不是等两个月以后看一个半成品。这个阶段我把大量精力花在了“录入体验”上。客户名称输入框加了拼音首字母联想联系人手机号加了格式校验跟进表单里把常用的下次动作做成标签微信、电话、见面、寄样点击一下就填入。这些细节看着小但是销售每天要录几十条每省一秒都是提升。同时我把 Excel 模板也做好了。老客户数据的导入我是先用 Python 脚本把几十个账号的客户数据清洗、去重后再批量导入。清洗规则很简单手机号去空格去横杠企业名称去末尾的有限公司之类的后缀做统一。导入不是一次性工作我把上传导入做成了功能后续新来的销售可以直接自己导。4.2 第二步权限和历史追溯同步上线主流程跑通以后第二步不是加报表而是上权限和历史记录。权限晚一步上的原因很简单前期人少大家互相都认识可以先不管等团队扩展到 10 个人以上不设权限就会开始出问题。我印象最深的一次是一个销售误删了另一个销售的跟进记录虽然数据恢复了但大家心里对系统产生不信任。历史追溯我用了一张统一的操作日志表记录谁在什么时间改了什么字段、旧值是什么、新值是什么。实现上用 SQLAlchemy 的事件钩子在每次更新后自动写入不用在各个接口里手写日志。event.listens_for(Customer, after_update) def log_customer_update(mapper, connection, target): connection.execute( operation_logs.insert().values( table_namecustomers, record_idtarget.id, changed_fieldsjson.dumps(target._changed), operator_idtarget._operator_id, created_atfunc.now() ) )这套日志上线之后很像“全场的监控摄像头”不是用来为难人而是给所有人一个安全感东西不会不明不白消失谁改的都能追溯到。销售团队的配合度反而提高了毕竟大家都怕背锅有据可查反而踏实。4.3 第三步打通邮件和日历让跟进有迹可循销售日常跟进大量依赖邮件但早期的 DeskcommCRM 和邮箱是分开的跟进记录全靠手工填写经常漏记。我后来做了一个轻量连接器用 IMAP 协议轮询销售指定的跟进邮箱把发给客户或客户发来的邮件主题、发件人、时间拉取下来自动匹配到对应的客户下生成一条跟进记录。这个功能解决了“跟进记录总是记不全”的老大难问题。只要邮件有主题、有收件人系统就能按域名匹配到客户。匹配不上的话就留在未关联邮件池里销售可以手工拖拽关联。准确性不用做到 100%能自动归集 80% 就已经省了大量体力活了。日历打通用的是标准的 CalDAV 协议把销售在系统里创建的下次联系时间同步到各自的公司日历这样即使销售不打开 CRM也能在日历上看到今天要联系谁。同步是单向的CRM 是本尊日历是投影避免双向同步产生死循环。这一步做完系统才真正嵌入了日常工作流不再是“多一个要维护的系统”。5. 常见问题与排查技巧实录5.1 重复客户数据为什么越用越多上线两个月后有个销售问我为什么我一搜“某某网络科技”能搜出五条几乎一样的客户我排查了一下问题出在查重提示被当成了摆设。弹窗提示“疑似重复”时下面给了“仍然创建”和“查看相似”两个按钮大部分人会图快直接点“仍然创建”。解决办法不是强制拦截而是让创建重复数据的成本变高。我把按钮改成了“我确认这是新客户”默认选中查看相似并把相似客户列表展示在创建表单旁边。改完以后重复产生率下降了七成。这个经验让我明白系统设计只能引导不能全靠强制好的交互让正确操作变得顺手。5.2 跟进记录丢失的排查过程有段时间运营反馈昨天的跟进记录今天看不到了。我查了数据库记录确实存在但客户列表页的最近跟进时间没有更新。再往下查发现是更新客户表updated_at和插入跟进记录这两个操作之间没有做事务。记录插进去了但更新客户表那步失败回滚了导致客户列表页展示的“最近跟进时间”还是旧的。这类问题的共同病根是跨表操作没有统一事务边界。我在跟进创建接口里把所有写操作包进同一个事务并且加了一个数据库约束跟进记录插入时间必须晚于客户当前最近跟进时间否则报错。用数据库兜底比在代码里小心保持顺序更可靠。从那以后没再出现过记录丢失或时间倒退的情况。5.3 客户全景视图加载慢的优化客户详情页我设计了一个“全景视图”把客户的基本信息、所有联系人、所有商机、时间线、下步计划全部放在一个页面里。最初实现是前端依次调五个接口每个接口查一遍数据库导致页面转圈时间超过三秒。销售的耐心撑不住三秒这个页面有段时间几乎没人看。我改成了后端聚合一次完成写一个get_customer_profile的接口用一条查询把客户信息取出来联系人、商机、待办分别走各自索引查询然后用 JSON 一次性返回。这种“读多写少”的场景牺牲一点灵活性换响应速度是完全值得的。优化以后这个接口稳定在 300ms 以内页面秒开。真正的经验是这类问题的排查顺序要反着来。别一上来就怀疑 SQL 慢先打开浏览器的网络面板看看发了多少个请求。大部分所谓“慢页面”是请求数量太多而不是单条 SQL 太慢。6. 复盘如果让我重来一次现在回头看 DeskcommCRM我会在三个地方做得更好。第一查重功能应该在第一版就做。我上线一个月以后才补上导致系统里积累了近千条重复数据清理成本比预防成本高得多。第二权限模型最好在一开始就统一封装。前期的简陋实现虽然跑得快但后期重构花的时间远超节省的时间。第三应该更早让销售参与验收。我习惯把一个功能做完善了再给业务看结果有两次方向理解偏差等于白做了两周功能这个教训很贵。但也有几件事我做得还算聪明。用配置表代替枚举让业务改流程完全不用发版用操作日志给所有人建立安全感邮件自动归集解决了跟进记录不全的老大难。这三个决策让 DeskcommCRM 没有在跑起来半年后变成一个新的“僵尸系统”。如果你也在考虑给自己的团队做一套内部客户管理工具我的建议很具体先找到那个“今天该联系谁”的答案其他所有功能都是这个核心的延伸。数据模型把客户、联系人、商机、跟进四个实体之间的关系理清楚权限一开始就统一设计查重导入这类基础功能尽量提前。把这四件事做好这套系统就能在团队里活很久。最后再分享一个个人体验内部工具做成什么样本质上是团队协作方式的一种确认。销售愿意每天打开的系统靠的不是功能堆叠而是它真的知道自己下一步该干什么。DeskcommCRM 能留下来恰恰是因为它记住了这一点。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。