自建CRM实践:从需求拆解到技术选型与踩坑实录
2026/9/16 13:06:42 网站建设 项目流程

DeskcommCRM 这个名字,是我在开发这套客户关系管理系统时临时起的。Desk 取“桌面工作台”的意思,comm 来自 communication,整个产品想解决的,就是让销售每天打开电脑第一眼就能看到今天该干什么,而不是在一堆 Excel 和聊天记录里来回翻找客户信息。说实话,市面上成熟的 CRM 产品不少,但大多要么功能臃肿到让人提不起配置的兴致,要么按坐席收费放到小团队身上贵得离谱。做 DeskcommCRM 的初衷很朴素:把销售跟进这件事,用一套轻量、够用、能自托管的系统管起来。

这篇内容我会把 DeskcommCRM 从需求拆解、技术选型、核心模块实现,到上线后遇到的若干坑完整记录一遍。适合正在评估自建 CRM 的团队,也适合想做一个“真正有人用”的内部系统的开发者参考。这里不写虚的,每一步都是我实际敲过、跑过、修过的。

1. 项目缘起与需求拆解

1.1 小团队用表格管客户,到底差在哪

一开始团队不到十个人,用共享表格管客户好像也没那么难受。无非是“公司名 + 联系人 + 电话 + 最近跟进”,一周汇总一次。但业务量上来之后,问题就藏不住了。

最典型的是数据归属混乱。客户是 A 先联系的,聊了两周觉得没戏就没再管,结果客户自己从官网表单进来,被 B 跟进了半个月后成交了。这时候该算谁的业绩?表格里没有明确的归属字段,也没有“公海客户”的流转机制,大家只能靠商量,商量不好就成矛盾。

其次是跟进记录散落在各个地方。微信里聊的算一段,邮件里写了一段,电话里说的内容压根没留痕。等接手前人客户的时候,除了表格里的公司名和手机号,对这个客户的偏好、卡点、报价历史几乎一无所知,等于冷启动重新聊。这个信息断层对小团队来说非常伤。

最后是跟进节奏完全靠脑子记。销售手里几十个在谈客户,哪个说过“下周再联系”,哪个答应“月底给答复”,全凭个人记忆。一忙就容易漏,一漏往往就把单子漏成了别人的。

所以 DeskcommCRM 的第一版需求,不是做一个大而全的销售管理平台,而是先把这三个问题解决:客户归属与流转、跟进记录留痕、跟进节奏提醒。其他的,像合同管理、回款统计、工单关联,都不在第一版考虑范围内。

1.2 产品定位:工作台优先,而不是功能堆砌

刚开始我也差点把功能范围铺开,什么 SFA、CPQ、BI 报表都想放进去。后来冷静下来,问了自己一个问题:如果团队里最不爱用系统的那个销售,打开这个产品 30 秒内能不能干完他当下要做的事?

答案是不能。所以我把产品定位全部压缩到“工作台”这个概念上。登录进去的第一个页面,不是数据大屏,不是客户列表,而是三块内容:今天需要跟进的客户、最近的待办跟进记录、新增的待分配线索。这个页面的设计原则只有一个——减少思考成本。

技术圈有个词叫“认知负担”,你让销售在一个系统里点四个菜单才能找到自己要跟进的客户,他第二次就不想用了。工作台本质上就是替使用者做了一个“优先级排序”的动作。你要打哪个电话、回哪条消息、处理哪条新增线索,打开系统直接就能看到。

这也决定了后续所有模块的排序逻辑:一切功能的评价标准,是能否帮助使用者减少操作步骤。一个功能如果只是“有”,但用起来要多点两次鼠标,那就宁可不要。这套取舍尺子伴随了整个开发过程,帮我砍掉了至少三成无意义的伪需求。

1.3 第一版功能清单与取舍逻辑

最终定下来的第一版本功能不多,但每项都对着刚才说的痛点:

  • 客户管理模块:包含客户的基础信息、归属销售、客户状态(潜在/跟进中/已成交/流失)、来源渠道、下次跟进时间。
  • 联系人子模块:一个客户下可以维护多个联系人,联系人独立存储跟进历史。
  • 跟进记录模块:支持文字记录跟进内容、标记跟进方式(电话/微信/邮件/线下拜访),并联动刷新下次跟进时间。
  • 待办提醒:工作台展示今日需跟进的客户列表,支持把跟进日期顺延。
  • 简单报表:每天新增客户数、跟进次数、成交数,按销售维度横向对比。

这里我刻意砍掉了“商机阶段管理”。本来想做类似 pipeline 的看板视图,把客户按阶段拖拽排序。但想一想,小团队的客户体量用看板意义不大,而且阶段字段如果设计不好,反而会变成销售为了移动卡片而移动。与其做一个大家不理解的抽象模型,不如直接做好“下次跟进时间”这个最基础的动作。

报表也只做最基础的每天几个数字,不做趋势预测,不做漏斗转化率。原因很简单:报表是给管理者看的,但录入数据的是销售。如果报表需求过度细化,录入负担就会上升,最终录入的数据质量下降,报表也成了摆设。先保数据质量,再谈数据挖掘。

2. 整体架构设计与技术选型

2.1 后端选型:基于开发效率与部署成本的平衡

个人项目最忌讳的就是技术栈选得又新又重,最后部署都成了负担。DeskcommCRM 的后端我选了 Python FastAPI + PostgreSQL。老实说,这种业务系统的核心逻辑是 CRUD 加上简单的关联查询,选什么语言差异不大,真正影响体验的是开发迭代速度和后续维护成本。

FastAPI 在这两点上很对我胃口。它的 Pydantic 模型可以直接当数据模型用,请求参数的校验和文档生成都是自动的。开发完一个接口,打开 /docs 就能直接调试,省掉了大量手工测试的功夫。相比 Django,FastAPI 更轻,没有强制一刀切的 admin 和 ORM 绑定,写起来更自由。

PostgreSQL 则是从第一天就确定了的。CRM 这种强关系型数据,用文档型数据库后面做关联查询会非常别扭。PG 的 JSONB 字段还能兼顾一些灵活配置的需求,比如客户的自定义属性,直接用 JSONB 存,不需要频繁改表结构。这个搭配在我实际开发中几乎没有遇到想换技术栈的冲动。

2.2 前端方案:Vue 3 + Element Plus 的实践感受

前端选了 Vue 3 + Element Plus。原因比较现实,这套组合的组件生态最成熟,表格、表单、日期选择器这些 CRM 里高频出现的组件开箱即用,不需要花时间造轮子。

页面结构上,我采用了左侧固定菜单、右侧内容区的经典布局。这个布局虽然不怎么新潮,但特别适合业务系统——操作路径短,功能层级清晰。前端路由层面做了按模块拆分,客户管理、跟进记录、报表、系统设置各是一级路由,用懒加载处理,保证首屏加载速度。

组件层面的一个体会是:像 CRM 这种大量操作表格的系统,真正决定好不好用的往往不是炫酷的交互效果,而是表格的稳定性。Element Plus 的表格组件在数据量不大的情况下表现很稳定,但一旦节点数量上来,渲染卡顿就会很明显。所以后面专门做了服务端分页 + 虚拟滚动优化,这个后面踩坑部分会细说。

2.3 数据库设计:核心表结构如何组织

DeskcommCRM 的数据模型我前后改了三版,最后稳定下来的核心表可以用下面几个来描述。先看客户表和联系人表:

CREATE TABLE companies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, industry VARCHAR(100), source VARCHAR(50), owner_id UUID REFERENCES users(id), status VARCHAR(20) DEFAULT 'potential', next_follow_up DATE, created_at TIMESTAMP DEFAULT now() ); CREATE TABLE contacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES companies(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, phone VARCHAR(50), email VARCHAR(255), position VARCHAR(100), created_at TIMESTAMP DEFAULT now() );

这里有一个关键设计决策:客户归属用了 owner_id 字段,没有单独建一张“客户-用户”的关联表。因为一个小团队里,一个客户在一个时间点只有一个负责人,搞复杂的多人协作共享反而会模糊责任边界。如果真要转交,直接更新 owner_id 就行,历史记录里会留下操作痕迹。

跟进记录表是客户信息的核心资产,我把它设计成轻量事件日志的格式:

CREATE TABLE activities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, contact_id UUID REFERENCES contacts(id), user_id UUID NOT NULL REFERENCES users(id), activity_type VARCHAR(30) NOT NULL, content TEXT, follow_up_date DATE, created_at TIMESTAMP DEFAULT now() );

每一条跟进记录都绑定客户、可选的联系人、操作人,以及最重要的 follow_up_date。这个字段是工作台“今日待办”的数据来源。查询今日待办时,只需要一条 SQL:找出 follow_up_date 等于今天且客户状态不是“已成交”“流失”的记录,然后按客户分组取最近一条。

这套结构有一个隐藏优势:跟进记录是追加式写入的,天然适应高并发插入,不会出现复杂事务竞争问题。而报表统计也只需要对 activities 表做 group by,比起拆成多条业务表再 join,统计逻辑简单清晰得多。

2.4 权限模型:轻量但满足基本隔离

权限设计我没有引入 RBAC 那种完善的用户-角色-权限三层模型,而是用了一个更贴合实际场景的简单方案:用户表加一个 role 字段,取值只有 admin 和 member 两种。

Admin 能看所有客户和数据报表,能管理系统设置、成员管理。Member 只能看自己名下的客户,报表中只能看自己的数据。这个模型虽然粗糙,但在十人以内的小团队足够用。如果后续真要做更细的权限,比如某个销售只能看自己行业下的客户,可以再引入数据权限范围字段,但当前阶段不做过度设计。

在实际开发中,权限校验是用 FastAPI 的依赖注入实现的。每个需要登录的接口都注入一个 get_current_user 依赖,从 token 中解析用户信息,再结合 role 做判断。只有管理员的接口多加一层 admin_required 依赖。这样代码非常干净,不用在业务逻辑里混入权限判断的条件分支。

3. 核心模块的实操实现

3.1 客户管理:列表筛选与批量操作的细节

客户列表是使用频率最高的页面,所以它的交互细节决定了一个 CRM 的“手感”。第一版只做了简单的表格 + 分页,后来发现销售找客户时经常要按多个条件组合筛选,就加了筛选器区域。

筛选条件包括:客户状态、来源渠道、归属销售、最近跟进时间范围、关键词。关键词搜索是对公司名做模糊匹配,后续升级到同时匹配联系人姓名和手机号。这里有个细节:关键词搜索如果直接在 SQL 里用 LIKE 拼接,在数据量上来后会非常慢,所以后来换成了 PostgreSQL 的 pg_trgm 索引,这个在踩坑部分详细展开。

批量操作方面,支持批量变更归属、批量变更状态、批量删除。之所以很早上这个功能,是因为公海客户流转需要一个高效的入口。比如某位销售离职了,管理员会一次性把他名下几十个客户转给其他同事,如果没有批量操作,一个个点会点到手抽筋。

页面交互上我坚持了“选中即反馈”的原则。勾选任意一行,页面顶部的批量操作按钮立即变为可用状态,并且显示已选数量。这个细节虽然小,但能显著降低误操作的概率。实际开发时用 Vue 的 computed 属性实时计算选中行数,并控制按钮的禁用状态,很好实现。

3.2 跟进记录与下次跟进时间的自动联动

跟进记录模块是整个系统的核心,也是我写得最认真的部分。核心逻辑很简单:销售在和客户沟通后,记录一段文字,选择沟通方式,然后设定一个“下次跟进日期”。提交后,客户列表里的 next_follow_up 字段自动更新,工作台第二天的待办列表就出现了这位客户。

这个逻辑的实现在后端是一个事务:

@router.post("/activities") async def create_activity(payload: ActivityCreate, current_user = Depends(get_current_user)): async with db.transaction(): activity = Activity( company_id=payload.company_id, contact_id=payload.contact_id, user_id=current_user.id, activity_type=payload.activity_type, content=payload.content, follow_up_date=payload.follow_up_date ) db.add(activity) await db.flush() company = await db.get(Company, payload.company_id) if company.owner_id == current_user.id or current_user.role == "admin": company.next_follow_up = payload.follow_up_date await db.commit() else: raise HTTPException(status_code=403, detail="无权修改该客户")

这里有一个容易忽略的边界条件:如果销售填写的下次跟进日期是过去的日期,说明这条记录可能是补录的。这种情况下,不应该让客户出现在“今日待办”里。所以查询待办列表时,SQL 条件是 follow_up_date 大于等于今天,而不是等于今天。

另一个经验是:跟进记录提交后,应该允许销售设置“标记完成”操作。有些客户聊完之后确实不需要再跟了,比如明确拒绝、竞品已签约。如果不提供完成入口,这些客户会永远躺在待办列表里,干扰每天的判断。我在客户状态里加了“流失”和“已成交”,跟进的 submit 表单里也加了“本次跟进后不再保留待办”的勾选项。

3.3 数据看板:不追求炫酷,先保证口径一致

DeskcommCRM 的看板页我用了三个简单的数字卡片加两张图表。数字卡片是:今日新增客户、今日跟进次数、本月成交数。图表则用 ECharts 画了最近 14 天的“跟进趋势折线图”和“销售跟进次数排行条形图”。

这里最值得分享的不是怎么画图,而是统计口径的制定。团队里对“新增客户”的定义就争执过一轮。是按创建时间算,还是按首次跟进时间算?最后统一成“按创建时间算,且创建后 24 小时内有跟进记录才算有效新增”。这样一来,批量导入的历史数据不会被错误地统计到当天的“新增”里,数据更有说服力。

销售排行是直接按 activities 表的 user_id 分组统计最近 30 天的记录数。这个指标虽然粗,但能很好地反映“活跃度”。管理层不需要看太复杂的转化漏斗,先把跟进量提上来,成交率自然会在后面体现。

报表页的性能优化也值得一提。最开始是每次打开页面都实时查询全表 group by,数据量到几万条后明显变慢。后来改成了每天晚上用 cron 任务把前一天的数据聚合成 daily_stats 表,报表页只查聚合后的数据。这条路的经验是:报表类数据的实时性要求没那么高,提前聚合远比实时计算靠谱。

3.4 待办提醒:工作台的核心价值所在

工作台是 DeskcommCRM 和普通客户表格最大的区别。登录后的默认页面,展示三块信息:今天需要跟进的客户列表、已逾期未处理的客户列表、待分配的新线索。

我在设计待办列表时特别加了一个“逾期”分类。很多系统的待办只显示今天,导致昨天的漏跟客户被静默掩盖。逾期列表就是把这些漏网的客户重新捞出来,并且用颜色标记逾期天数。逾期越久颜色越深,给销售一个直观的压力反馈。

待办列表还有一个很实用的功能:快速录入“已联系”。销售点进待办后,可以不用跳转到客户详情页,直接在卡片上展开一个迷你录入框,填一句跟进内容和下次跟进时间,提交即可。这个设计把“看到待办 → 完成记录”的路径压到了最短,实测团队成员使用意愿明显提高。

技术上,待办列表是一个独立的查询接口,根据当前登录用户的 ID 去 activities 表找最新的 follow_up_date,再和今天的日期比较。查询结果按“逾期优先,今天次之,未来排序”的规则排列。这个排序规则用 SQL 的 CASE WHEN 实现,逻辑清晰,性能也不错。

4. 上线后的踩坑实录与排查技巧

4.1 多人同时编辑客户信息导致的覆盖问题

上线两周后遇到一个典型问题:销售 A 和销售 B 同时打开同一个客户的编辑页,A 先保存了联系方式,B 后保存却把 A 改的号码覆盖掉了。原因是我保存接口用的更新逻辑是整体更新,前端把整个表单对象传回来,后端直接 UPDATE,缺失的字段就会写成空。

这个问题最省事的解法是在更新前先做一次字段级别的 diff,只更新变化的字段。但这样又会导致另一个问题:A 和 B 修改的是不同字段时,diff 结果分别只更新自己改的字段,看起来没问题;可如果 B 的页面打开时间太早,他看不到 A 刚改的字段,盲目提交别的修改时没有冲突提示,仍然可能用旧数据覆盖掉别人的新数据。

最终的方案是加乐观锁:在 companies 表增加一个 version 字段,每次更新时带上当前读取到的 version,UPDATE 的时候 SET version = version + 1 WHERE id = $1 AND version = $2。如果影响行数为 0,说明记录已经被别人更新过了,直接返回 409 冲突,前端弹窗提示“该客户信息已被他人修改,请刷新后重试”。这个方案实现成本低,却避免了绝大多数编辑覆盖问题。

4.2 客户列表响应变慢:从分页到索引的排查过程

业务跑了三个月,客户数据到了一万多条,列表页开始出现明显的卡顿。一开始怀疑是数据库慢,打开慢查询日志发现单个列表查询耗时最高的能到 1.8 秒,这在局域网应用里是不可接受的。

首要问题是前端一次性渲染了全部筛选结果。虽然接口做了分页,但默认每页显示 20 条,不至于一次性渲染几万条。真正的问题出在关键词搜索上:销售经常用手机号后四位或联系人名字搜索客户,这个查询条件需要在 contacts 表里用 LIKE 匹配,而 contacts 表和 companies 表之间的关联查询没有合适的索引,导致全表扫描。

针对这个场景,我做了两件事。第一,给 companies.name、contacts.name、contacts.phone 三个字段加了 pg_trgm 的 GIN 索引:

CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX idx_companies_name_trgm ON companies USING gin (name gin_trgm_ops); CREATE INDEX idx_contacts_name_trgm ON contacts USING gin (name gin_trgm_ops); CREATE INDEX idx_contacts_phone_trgm ON contacts USING gin (phone gin_trgm_ops);

第二,前端把默认分页改成服务端分页,并且把远程筛选放在请求参数中传递给后端,不再前端全量过滤。

修改后,同样的搜索条件耗时从 1.8 秒降到 100 毫秒左右。这个优化效果非常明显,教训也很简单:不要坐在那里猜性能瓶颈,用慢查询日志把真正的慢语句找出来,然后对着执行计划做优化。

4.3 定时提醒服务的不稳定与重复发送问题

待办提醒除了在工作台展示,我还想加一个主动推送能力,每天早上 9 点给有当日待办的销售发一封邮件提醒。一开始的实现方式特别简单:写一个 cron 脚本,每天早上扫描 activities 表,把当天有跟进任务的用户和客户列表汇总,然后发邮件。

跑了一周后发现问题:有两天邮件重复发送了,还有一天完全没发。排查下来,没发的那天是服务器重启时 cron 服务没自动拉起;重复发送则是因为长时间运行的脚本里,邮件发送列表没有做幂等,某次网络超时导致重试,重试时没有检查发送记录。

解决重复发送的方案是引入一个 user_notify_log 表,记录每个用户每个自然日的发送状态,发送前先查表判断是否已发过。邮件发送部分封装了幂等逻辑:先插入日志记录,再发送邮件,发送成功就把日志状态置为 success。如果发送失败,状态保留为 pending,下次重试时会先看到已有记录,避免重复发。

这个坑提醒了我:任何定时任务都必须把“失败重试”和“重复执行”当作默认条件来设计,环境永远比你想象的更不稳定。

4.4 数据备份与还原演练

做内部系统的普遍问题是不重视备份,觉得数据量小、数据库不会挂。结果真遇到一次误操作,有人写了一个批量更新脚本,本来只想更新 10 条测试数据,结果 where 条件少写了一行,把整个表的状态字段都改了。当时又没有开启 PostgreSQL 的 PITR 时间点恢复,只能从前一天晚上的全量备份里找回数据,丢失了当天一个白天的变更。

这次教训之后,我搭了一套简单但完整的备份机制:

  • 每天凌晨 2 点用 pg_dump 做全量备份,保留最近 30 天。
  • 开启 PostgreSQL 的 WAL 归档,配合 wal-g 做增量备份,可以恢复到任意时间点。
  • 每个季度做一次完整的还原演练,确保备份文件真的能恢复,而不是躺在磁盘上吃灰。

这里特别想提醒一下:备份的目的不是“有备份文件”,而是“能在需要的时候成功还原”。不做还原演练的备份,约等于没有备份。我亲眼见过团队从云上下载备份文件后才发现备份文件是损坏的,那种感觉非常绝望。

5. 部署交付与升级规划

5.1 部署方式:Docker Compose 单机部署

DeskcommCRM 的部署我选择了最简单的 Docker Compose 单机模式。一个 docker-compose.yml 里编排了三个服务:postgres 数据库、后端 API 服务、前端 Nginx 静态文件服务。这个方案对于十人团队的使用量绰绰有余,单机部署也避免了 Kubernetes 那套复杂运维带来的维护成本。

实际部署时有一个容易踩的坑:PostgreSQL 的持久化数据卷如果没有正确挂载,容器一重建数据就全丢了。所以我的 compose 文件里单独给 postgres 挂了一个 named volume,并且把数据库连接串通过环境变量传给后端容器。前端构建的时候把 Nginx 单页应用路由配好,刷新页面不会 404。

整个部署过程基本上就是:服务器上装好 Docker 和 Compose,把代码拉下来,docker compose up -d 构建启动,再用 Nginx 反代到容器端口,申请一张免费的 SSL 证书,域名访问就通了。整个过程不到半小时,后续升级只需替换镜像重新启动。

5.2 敏感信息管理与合规事项

CRM 里存着客户的姓名、手机号、微信、地址这些个人数据,安全这块不能马虎。我在系统里做了几层措施:

  • 所有密码用 bcrypt 哈希存储,数据库泄露也无法还原原始密码。
  • 接口全部走 HTTPS,防止传输过程被截获。
  • 手机号等敏感字段在列表页做了脱敏展示,详情页才显示完整号码。
  • 系统操作日志记录了谁在什么时间查看了哪个客户的详细资料,便于事后审计。

这里多说一句,无论你的系统是自用还是商用,只要是收集、存储了中国境内用户的个人信息,就必须遵守相应的数据安全和个人信息保护要求。哪怕只是一个十人团队的自建系统,也建议把“最小够用”原则落实到权限和展示层,不该看的字段不要让每个角色都能看到,降低系统后台被滥用导致的风险。

5.3 后续演进:从内部工具到可产品化的方向

DeskcommCRM 做到这个阶段,已经稳定服务团队半年多了。我后续考虑的几个演进方向,也是这个产品真正走向产品化的几个关键点:

第一是增强“公海客户自动回收”机制。目前客户只有在手动释放或者负责人离职时才会回到公海,后续想做成长期未跟进自动回收,比如超过 30 天没有跟进记录的客户自动回到公海,让线索流转起来。

第二是移动端适配。目前系统纯 Web,对经常在外面跑的销售不太友好。做不了完整的 APP,至少要做一套移动端 H5,支持快速查看今日待办、记录跟进。这个需求我评估后认为是动作价值最高的改进项。

第三是客户导入导出的标准化。从 Excel 导入是第一批用户最常用的功能,但字段映射不完善,导入后经常出现格式问题。后续打算做一个导入模板校验器,在正式导入前预览并提示清洗结果。

产品化转型方面,如果要把这套系统开放给其他小团队使用,还需要补上多租户隔离、SaaS 支付通道、更完善的审计日志等能力。这些功能本身并不复杂,但每一项都需要仔细设计,不能为了做而做。

6. 写在最后的一点体会

DeskcommCRM 从立项到稳定运行,前后大概用了四个月。坦白说,这套系统的技术难度只能说中等,真正花心思的不是某段算法或者某个高并发场景,而是如何把“销售每天打开系统就能知道该干什么”这件事做顺。功能越简单,背后的取舍越需要克制。

我最大的收获是:内部系统开发,业务逻辑的理解永远比技术实现重要。听不懂销售们“公海”“转交”“跟单”这些词背后的真实痛点,做出来的系统哪怕界面再漂亮也是一个摆设。反过来,只要真正解决了“少翻几次表格、少漏一个跟进”这样的实际问题,哪怕交互朴素一点,同事也会天天打开它。

如果这篇记录能让你在评估自建客户管理系统时少走一点弯路,我就觉得值了。最后再给一个小建议:如果你的团队也准备做类似的系统,先别急着选型和写代码,抽一天时间跟在销售旁边看他怎么干活,那才是你需求文档真正的来源。

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

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

立即咨询