从最早用 Excel 管理客户,到后来咬牙上了一个轻量级 SaaS CRM,再到最后我们团队决定自己动手写一套 DeskcommCRM——这条路走下来,我最大的感受是:CRM 不是买回来的工具,而是长出来的业务系统。如果你正好在纠结“要不要自研一套 CRM”,或者已经被现成产品的字段限制、权限漏洞和集成成本搞得焦头烂额,这篇文章应该能给你一些实在的参考。
DeskcommCRM 是我们在 2024 年下半年启动、2025 年初正式上线的内部客户管理系统,核心解决三件事:销售管道可视化、跨部门客户数据共享、以及日常跟进任务的自动化提醒。名字里的 Deskcomm 是我们公司的业务线代号,所以这套系统从一开始就没打算对外商业化,完全是冲着“业务部门要什么,我们就改什么”这个目标去的。也正是因为这样,它在技术选型、数据模型和权限设计上,跟市面上大多数通用 CRM 有着相当明显的差异。
1. 为什么我们把用了三年的 SaaS CRM 换成了自研系统
1.1 现成产品用着用着就撞到了天花板
在决定自研之前,我们其实正正经经地用过一款在国内挺知名的 SaaS CRM,年费不高,功能也不少,销售团队上手也比较快。但用了大概一年半之后,问题开始集中爆发。
最先出问题的是字段灵活性。我们的销售流程里有大量“只看一次就回不来”的场景,比如客户处于“试用阶段”时需要一个自定义的“试用版本号”字段,项目结束后这个字段对整个团队来说就毫无意义了。SaaS 产品的字段管理虽然支持自定义,但往往有数量上限、类型限制,而且修改历史字段的展示逻辑很痛苦。更麻烦的是,跨对象关联查询在 SaaS 里基本做不了太深,比如“找出所有在 30 天内做过 3 次跟进但没进入下一阶段的试用客户”这类需求,要么靠导出到 Excel 手动筛,要么就得等厂商排期开发。
第二个大问题是数据所有权。我们用 SaaS CRM 期间,销售主管要求看到每个销售人员的跟进记录和成交预估,但系统自带的报表模块对“团队数据穿透查看”的支持非常弱。有些销售为了月底报表好看,干脆把跟进记录补写在备注里,也不愿意填到结构化字段中,因为大家心里都清楚——这数据进了 SaaS 系统,所有权和导出控制都不在自己手里。
第三个问题才是压垮骆驼的最后一根稻草。我们的客户成功团队需要对接企业微信的聊天记录和通话录音,销售部的日常流程又强依赖内部工单系统。数据同步靠写脚本调 API,但 SaaS CRM 的 API 配额有限,同步频率低,经常出现抖音上刚聊完的客户,晚上才能在 CRM 里看到聊天记录。业务团队开始吵,研发团队也开始不爽,一个 CRM 项目做成了夹在供应商 API 和业务需求之间的传话筒。
1.2 DeskcommCRM 的目标边界:只做业务真正需要的五件事
在立项会上,我们给自己定了一条铁律:不要做一个“小 Salesforce”,只做业务团队每周至少用五次的模块。最终敲定的边界非常明确:
- 联系人管理:客户公司、联系人、标签、来源渠道、自定义字段。
- 销售管道:阶段、金额、预计成交日期、阶段变更历史。
- 跟进任务:电话、拜访、演示、合同审批,全部带提醒。
- 报表看板:团队漏斗、销售预测、跟进及时率。
- 权限与审计:销售数据隔离、操作日志可回溯。
不做在线聊天、不做营销邮件群发、不做复杂工单系统,也不做移动 App——这五件事,是当时业务团队说了三遍以上“必须要”的需求。我们宁可把桌面端体验做精细,也不去铺那些看起来热闹但用不上的功能。
定完边界之后,我们做了一次成本评估:SaaS CRM 一年的费用大概是 6 万块,自研按两名后端、一名前端全职投入 4 个月算,人力成本远高于此。但考虑到 API 配额、字段扩展和企业微信深度集成这些长期收益,团队一致认为自研的 ROI 是正的。事实证明,这个判断在一年后就得到了验证——我们后来增加的“客户健康度评分”功能,如果真的放在 SaaS 系统上,又是一轮漫长的需求拉锯战。
1.3 技术选型里我们权衡过三条路
技术栈的选择,我们实际上在立项后就开了两次会。团队成员背景偏全栈,有从 Java 转过来的,也有一直用 Node.js 的,所以讨论比一般团队更激烈一点。
当时摆在桌上的方案有三个:
| 方案 | 后端 | 前端 | 数据库 | 核心优势 |
|---|---|---|---|---|
| A | Python / Django | Vue 3 | PostgreSQL | 自带 Admin 和权限,开发快 |
| B | Node.js / NestJS | Vue 3 | PostgreSQL | 前后端语言统一,社区资源多 |
| C | Java / Spring Boot | React | MySQL | 团队最熟,招聘容易 |
最后选 B 的原因很实际:我们团队里前端有 4 人,后端只有 2 人,用 NestJS 可以把 DTO 校验、依赖注入、模块化开发这些工程规范交给框架来约束,后端的开发量相对可控。数据库统一选 PostgreSQL,因为它的 JSONB 类型在实现自定义字段时比 MySQL 友好太多,后面数据模型那部分我会重点展开讲。
数据库迁移工具用的 TypeORM Migration,接口文档直接上 Swagger。部署方面当时很简单,先用 Docker Compose 跑在内部测试服务器上,稳定后再上 Kubernetes。这套组合可能不是最酷的,但是在大约四个月的项目周期里,它让我们没在环境问题上耗掉一丝时间。
2. 数据模型设计:CRM 的骨架决定业务上限
2.1 六大核心表的字段设计与关系
CRM 系统的数据模型,本质上回答一个问题:一段销售记录,从进入系统到成交归档,需要经过哪些实体、产生哪些关联。我们最终收敛出六张核心表,基本覆盖了 90% 的业务操作。
第一张是companies表,也就是客户企业。字段不追求多,但必须有唯一性约束和法律实体识别。它包含:公司名称、统一社会信用代码、所属行业、规模区间、来源渠道、地址、官网、创建人、负责人。
第二张是contacts表,存的是具体联系人。每个联系人必须关联一个公司(允许关联到“未知公司”这个占位记录),包含姓名、职位、电话、微信、邮箱、微信企业号 ID、备注、是否决策人。
第三张是deals表,也就是商机表,也是销售管道的核心。字段包括:商机名称、关联公司、金额、预计成交日期、当前阶段、赢率、负责人、创建人、关闭原因、实际成交金额。
第四张是activities表,记录所有跟进动作。每次电话、拜访、邮件、演示都算一条 activity,带type、subject、content、start_at、end_at、owner_id和可选的deal_id。
第五张是tasks表,用来做待办和提醒。它跟 activity 的区别是:activity 是已发生的记录,task 是计划要做的事,所以 tasks 表里有due_at、remind_at、done_at。
第六张是users表,加上它的延伸表user_roles和role_permissions,负责权限基础数据。
这六张表的关系非常清晰:一个公司有多个联系人,多个商机;一个联系人和商机的关联需要通过deal_contacts这张关系表,因为一个商机的参与人可能不止一个。task 和 activity 都通过owner_id找到对应的 user,同时可选地通过deal_id和contact_id挂到具体业务对象上。
2.2 自定义字段的存储方案:EAV 老路和 JSONB 新路
自定义字段是 CRM 的刚需,但实现方式决定后续开发是顺风还是逆风。我们最初的方案参考了经典 EAV(Entity-Attribute-Value)设计,也就是把字段定义存在一张custom_fields表里,把字段值存在另一张custom_field_values表里,每行存一个字段的值。好处是结构最灵活,坏处是当列表页需要展示 20 个自定义字段时,要 JOIN 20 次,SQL 写起来要人命,性能也会随数据量增长快速劣化。
后来我们换成了 PostgreSQL 的 JSONB 方案。具体做法是:在每张核心业务表上加一个custom_data字段,类型是 JSONB。字段定义存一张company_field_configs表,里面记录字段名、字段类型、是否必填、可选项配置。查询的时候,直接在 JSONB 上做->>操作符提取值。
-- 查询所有“试用版本号”等于 v2.3 的公司 SELECT * FROM companies WHERE custom_data->>'trial_version' = 'v2.3';JSONB 方案在写入时需要多一道校验逻辑:根据company_field_configs表里的配置,校验传入的custom_data是否包含必填字段、类型是否正确。我们把这部分做成了通用工具函数,在 NestJS 的 Service 层调用,代码量不大,但保证了数据洁癖。
如果你也想在项目里用 JSONB 做自定义字段,我建议一开始就把应该做成系统字段和自定义字段的边界划清楚。比如“公司名称”“负责人”这种所有业务线都要用到的,必须是固定字段,不能放进 JSONB,否则索引、权限、排序都会变得异常别扭。
2.3 索引策略与数据约束:从丁零当啷的 SQL 到跨模块查询
数据模型设计完之后,真正让系统跑得顺的是索引和约束。我们在测试环境里导入了一批真实的脱敏数据,大概有 10 万条 company、30 万条 contact、20 万条 deal,然后开始调索引。
最常用的查询场景有两个。第一个是列表页分页查询,条件通常包含负责人 ID、公司名称模糊搜索、阶段、更新时间范围。针对这个场景,我们建了复合索引(owner_id, updated_at DESC),并把公司名称的模糊搜索改成了 PostgreSQL 的pg_trgmGIN 索引,否则LIKE '%关键词%'会全表扫描。
CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX idx_companies_name_trgm ON companies USING gin (name gin_trgm_ops);第二个场景是报表的聚合查询,比如“每个销售在每一阶段的商机总金额”。这种查询对deals表的stage、owner_id、amount三列有固定过滤条件,我们加了一个覆盖索引(owner_id, stage, amount),让查询可以走 Index Only Scan,减少回表。
约束方面,有几个关键设计是上线后才补上的。
- 金额字段设了
CHECK (amount >= 0),防止负数金额进入报表。 - 阶段变更历史表加了
version字段,同一张 deal 的同一阶段只允许出现一次,如果重复进入,需要先走“重开”流程。 - 联系人的手机号做了格式校验,但允许空值,因为不是每个联系人都能拿到手机号。
3. 核心模块实现:销售管道、联系人管理和活动记录的实操细节
3.1 销售管道(Pipeline)的状态机设计
销售管道在整个系统里是业务属性最强的模块,也是销售团队每天打开系统第一时间看的页面。我们的管道有六个阶段:初次沟通、需求确认、方案演示、商务谈判、合同审批、赢单/输单。前五个是进行中状态,最后两个是终态。
在数据库里,阶段字段用VARCHAR存字符串标识,但业务操作上不能直接 UPDATE 这个字段就完事,必须走一个统一的状态机转移接口。状态机里定义了两类规则:合法流转路径、状态迁移的副作用。
合法流转路径用一张配置表存起来:
stage_from -> stage_to initial-communication -> needs-confirmation needs-confirmation -> demo demo -> negotiation negotiation -> contract contract -> won contract -> lost negotiation -> lost demo -> lost注意这里允许“跳过阶段”的流转,比如初次沟通后直接到商务谈判,这在紧急客户场景里确实会出现。但禁止了逆向流转,比如从谈判回退到需求确认,必须走“阶段回退申请”审批,保证销售漏斗数据的可信度。
状态迁移的副作用会触发两个动作:自动记录一条activities历史,以及更新 deal 上冗余保存的stage_changed_at字段。这个字段很关键,因为我们的“跟进及时率”指标就定义成“当前阶段停留时间超过 N 天的商机数量”,没有这个时间戳,报表逻辑会绕一大圈。
赢单和输单的处理也有讲究。赢单时,系统把deal.amount复制到closed_amount,同时自动检查并更新关联公司的“活跃状态”。输单时,必须填写输单原因(价格、竞品、需求消失、无决策权),这个原因是业务分析的重要维度,不能留空。
3.2 联系人/公司去重合并逻辑:看起来很土,但真的不能不做
数据重复是 CRM 系统的头号污染源。我们的数据来源有两个:手动录入和从企业微信导入。手动录入的场景里,销售经常把同一家公司写成“某某科技有限公司”和“某某科技责任有限公司”,两个名称都进了系统,后续的跟进记录全部分散,报表也会出现同一个客户算两遍的闹剧。
去重逻辑我们分两层做。
第一层是录入时实时查重。在新建公司和联系人的表单提交前,前端调用后端的POST /companies/check-duplicate接口,后端用pg_trgm的相似度函数similarity(company.name, :input_name)跑一遍,相似度超过 0.7 的就返回候选列表,提示用户选择“关联到已有公司”而不是新建。
// NestJS 中的查重服务片段 async checkDuplicate(name: string): Promise<Company[]> { return this.companyRepo .createQueryBuilder('company') .where('similarity(company.name, :name) > 0.7', { name }) .orderBy('similarity(company.name, :name)', 'DESC') .limit(5) .getMany(); }第二层是定期的合并任务。每周日凌晨跑一个批处理,扫描所有相似度超过阈值的公司对,如果两个公司都没有关联 deal 和 activities,自动合并;有关联的话,挂到人工审核队列。之所以不全程自动化,是怕误合并把销售的数据串到别人头上,这个风险不能赌。
联系人去重相对简单,主要按手机号和微信号做精确匹配。但这里有个特殊场景:同一个客户,一个账号是个人微信,一个账号是企业微信,系统里可能出现两条联系人记录。我们干脆新增了一个字段contact_source,标记是“个人微信导入”还是“企业微信导入”,展示层让用户自己决定是否合并,不强行统一。
3.3 任务和活动的时间线处理:把零散动作串成一条线
一个客户从建立到成交,中间的跟进动作可能超过 20 次。如果没有一个清晰的时间线展示,销售自己都记不住上次聊了什么,更别说替班的同事接手之后怎么快速上平。
DeskcommCRM 的时间线设计基于activities表,所有类型的事件都落到这张表里。展示时,按created_at倒序,每条活动显示类型标签、标题、内容、操作人和时间。电话事件还要显示通话时长,如果是用企业微信拨打的,自动同步录音链接。
任务模块的设计要考虑的是“提醒”而不是“记录”。很多 CRM 的 task 模块做成了单纯的待办列表,没有提醒能力,销售根本不会主动去看。我们的做法是:tasks表里设置了remind_at字段,后台有一个定时任务每分钟扫描一次,把remind_at在 5 分钟内的未完成任务推送给对应负责人的企微工作通知,内容包含任务内容、关联客户名和跳转链接。
除了给用户提醒,还有一类任务需要自动创建。比如最新的策略是:每次通话结束后,如果客户提到“需要找技术确认接口”,系统要通过关键词匹配自动给销售创建一个“向客户技术确认接口细节”的任务。这个功能上线后,忘跟进的投诉明显变少。其实实现逻辑不复杂,正则匹配加固定模板,但收益却非常高。
4. 权限体系设计:团队协作不越权的关键
4.1 基于角色的访问控制(RBAC)落地
CRM 里权限没做好,轻则数据混乱,重则销售离职后直接带走客户。我们的权限体系分成三层:功能权限、数据权限、操作日志。
功能权限用 RBAC 模型实现。系统内置五个角色:超级管理员、销售主管、销售人员、客户成功、只读访客。每个角色挂一个权限码集合,比如deal:read、deal:create、deal:update_stage、deal:delete、report:view、settings:manage。权限码统一用字符串,存在role_permissions表里。
判断权限时,NestJS 的守卫层会读取当前用户的权限码数组,然后和路由装饰器里声明的@RequirePermissions('deal:update_stage')做比对。这套思路是标准的 Casbin 简化版,没有引入额外依赖,实现起来也很直观。
值得注意的一点是:功能权限并不能真正保障数据安全。一个销售即使有deal:read权限,他也不应该看到其他销售跟进的商机详情。这就需要数据权限出场。
4.2 数据级权限:谁能看到谁的客户
数据权限的设计,我们参考了国内大厂的做法,按“数据归属 + 可见范围”两个维度控制。
每个 company、contact、deal 都有一个owner_id字段,表示当前负责人。对于 deal 的列表查询,系统会根据当前用户的角色和部门动态生成查询条件:
- 销售人员:只看到自己创建的 deal,以及自己是被共享人的 deal。
- 销售主管:看到自己直属团队的 deal,也就是
owner_id在我管理的用户 ID 列表内。 - 客户成功:看到所有进入“方案演示”阶段及以后的 deal,因为协作需要。
- 超级管理员:全部数据。
部门维度我们当时没做。因为团队规模不到 50 人,一个销售主管管 6-8 人,用“直属团队”就够了。但如果你在更大的组织里落地这套逻辑,建议在 users 表上增加department_id,再做一层部门可见范围的叠加判断。
共享逻辑单独说一下。销售经常需要临时把某个客户共享给售前工程师看。我们在deal_shares表里记录deal_id、user_id、expired_at。查询 deal 详情时,先判断owner_id == current_user,不满足就查deal_shares,再不行就检查角色是否满足特权条件。这个顺序不能反,否则容易误放权限。
4.3 审计日志与操作回溯:上线第三周救了我们一次
审计日志最早是运维团队要求的,说“出了问题不能查不到”。我原本打算草草往表里扔点 JSON,没想到上线第三周就派上了大用场。
那周有销售反馈说自己的 deal 金额被人改掉了,但系统里没有任何记录。我们当时只有updated_at字段,完全无法判断谁改的、改了什么。后来紧急给deals表加了一个deal_histories表,每次 UPDATE 都通过触发器记录变更前后的关键字段:金额、阶段、负责人、预计成交日期。
触发器的实现方式是现学现卖:
CREATE OR REPLACE FUNCTION deal_change_trigger() RETURNS TRIGGER AS $$ BEGIN INSERT INTO deal_histories(deal_id, changed_at, changed_by, old_amount, new_amount, old_stage, new_stage, old_owner_id, new_owner_id) VALUES ( OLD.id, NOW(), current_setting('app.current_user_id')::uuid, OLD.amount, NEW.amount, OLD.stage, NEW.stage, OLD.owner_id, NEW.owner_id ); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_deal_change AFTER UPDATE ON deals FOR EACH ROW EXECUTE FUNCTION deal_change_trigger();这里有几个细节:app.current_user_id是我们在 NestJS 里通过SET LOCAL app.current_user_id = '...'在事务里设置的,必须用current_setting读取,因为参数带点号,set_config的命名空间规则很重要。另外注意触发器必须判断 NEW 和 OLD 的值是不是真的不一样,否则每次更新都会插入一条无意义的日志。
现在我们的审计日志不仅用于排查问题,还在部门复盘时用来分析“这个 deal 为什么在某个阶段停留了 20 天”,已经变成了业务数据分析的一部分。
5. 集成层与自动化:让 CRM 真正成为工作台
5.1 邮件收发与日历同步:打通个人工具和企业协作
CRM 的价值有相当大一部分来自集成,但我们一开始没有做完整的企业微信全套对接,而是从两个最痛的点切入:邮件和日历。
邮件集成的方案用的是 IMAP 监听。每个销售在自己的邮箱配置里填入 IMAP 地址、账号和授权码,系统用imap-client库每 30 秒扫描一次收件箱,把主题包含客户公司名或者发件人匹配联系人的邮件抓取出来,新建一条 activity。抓取邮件时做了两个保护:只添加到曾经有往来记录的 company 下面,且不会自动删除用户邮箱里的邮件,只是在 CRM 里加一条索引。
日历同步稍微复杂一点。我们团队内部用的是企业微信日历,外部销售用的是自建 CalDAV。因为确实没有精力同时兼容两个协议,最后决定先做 CalDAV。用户在设置页关联 CalDAV 账号后,系统会每天把他当天的会议日程拉取到 CRM 时间线里。不直接双向同步,因为双向同步涉及到的冲突处理逻辑太多,第一版先做到“可见”就好。
“可见”这个诉求是销售提的,他们说:客户打电话来说今天下午三点有个会,我经常忘记,如果 CRM 能在我点开客户的时候看到今天跟他有什么会,就不会漏了。就这么一个简单的展示功能,上线后使用率非常高。
5.2 自动化规则:状态变更触发动作
自动化功能延展了 CRM 的主动性。我们的自动化引擎核心很简单:事件(Event)、条件(Condition)、动作(Action),简称 ECA。
目前跑的自动化规则有六条,其中效果最明显的是“商机阶段变更通知”和“跟进超时提醒”。
商机阶段变更通知:当 deal 从“需求确认”进入“方案演示”阶段时,系统自动给售前工程师发送一个企微工作通知,附上 deal 链接和相关链接。这个规则把原本需要人工转告的流程给消灭了。
跟进超时提醒:每天晚上 8 点,系统扫描所有“当前阶段停留超过 7 天且今天没有 activity”的 deal,给负责人发一条提醒,同时给自己的主管抄送一条。实施后的两周,整体漏斗的推进速度提高了约 15%,业务主管对这个改变非常满意。
自动化规则的实现没有引入独立的工作流引擎,就是定时任务加简单的条件判断。NestJS 的@Cron装饰器足够用了。
@Cron(CronExpression.EVERY_MINUTE) async runPendingRules() { const pendingActivities = await this.activityService.getCreatedInLastMinute(); for (const activity of pendingActivities) { for (const rule of await this.ruleService.getAllEnabled()) { if (rule.matches(activity)) { await this.ruleExecutor.execute(rule, activity); } } } }这套轮询方案在每分钟几千条 activity 量级下没有任何压力,但如果你预估的数据量再大一个量级,建议用消息队列触发规则,别靠扫表硬扛。
6. 部署运维与性能优化:自研系统上线后的 90 天
6.1 从 Docker Compose 到 Kubernetes:小步慢跑的部署演进
上线初期,整个系统就是四个容器:Nginx(静态资源+反向代理)、NestJS 后端、PostgreSQL、Redis(缓存和任务中间态)。docker-compose.yml 里把端口、环境变量、卷映射都写清楚,一台 4C8G 的测试服务器就能跑。
这个阶段最大的心得是:数据库的持久化卷一定要提前配上volume,否则一次docker compose down就把数据清零了。我们第一次部署时就吃过这个亏,亏在一个“顺手重启”的命令上。后来加了pg_dump的定时任务,才算松了口气。
上线一个半月后,用户量上来,PostgreSQL 的 CPU 开始出现持续 80% 的情况。我们把部署切换到了 Kubernetes,通过 HPA 自动扩缩容后端实例,数据库做了一主一从的读写分离。切换本身不算难,真正麻烦的是把 NestJS 的无状态化改造完整落地——比如本地文件缓存全部迁到 Redis,日志统一输出到标准输出由 EFK 收集,这个改造花了大概两周。
6.2 查询性能优化与缓存策略:20 万行以后才开始真刀真枪
数据量到了 20 万行之后,之前慢得不够明显的查询开始变得肉眼可见。我们用pg_stat_statements插件找出 TOP 5 慢查询,发现问题集中在两类:一类是列表页的分页排序,另一类是报表页的聚合。
分页排序优化做了两件事。第一件是把前端的分页参数从offset改成keyset分页,也就是基于updated_at和id的游标分页。它的好处是,越往后翻页性能越稳定,不会因为 offset 大到几十万行而全表扫描。第二件是给列表页的所有“筛选条件”组合建了复合索引,前文提到的(owner_id, updated_at)就是这里的产物。
报表聚合的优化更走了一点捷径。有些统计指标(比如团队月底 KPI 达成率)每次刷新报表页面都要跑到deals表聚合一遍,数据量大时响应时间接近 4 秒。我们加了一层 Redis 缓存,key 设计成report:kpi:2025:01,过期时间设 30 分钟。报表数据本身不需要实时到秒级,30 分钟的延迟完全可接受,但查询响应时间直接从 4 秒降到了 200 毫秒以内。
Redis 缓存有几个必须避开的坑。第一个是缓存穿透——大量请求查同一个不存在的 key 时会直接打到数据库,我们给空结果也做了一层“空缓存”,过期时间 2 分钟。第二个是缓存雪崩——所有 key 同一时间过期会导致瞬间压力,我们在 key 过期时间上加了一个随机 0-60 秒的偏移量。
6.3 备份、监控与容灾:没有事故的时候最容易忽视它
备份这件事,我们在上线当天就做了,但做完之后整整两个星期没检查过备份是否可用。在第二次复盘时,运维同事提议做一次“备份演练”,结果发现 pg_dump 的备份文件里缺了最新的 3 天数据,原因是 Cron 里挂的容器名字变了,备份任务其实一直在报错,只是错误没有同步给任何人看到。
从那以后,我们的备份策略改成三层:
- PostgreSQL 每天凌晨 1 点做一次全量
pg_dump,保留 14 天。 - WAL 日志实时归档到外部存储,确保最多丢失 5 分钟的数据。
- 每周做一次恢复演练,确保备份真的能在空库上恢复出可用系统。
监控方面,Prometheus + Grafana 是标配。我们自己额外写了一个接口/health/ready,不仅检查数据库连接,还检查 Redis 连通性和关键定时任务最近一次执行时间。这个接口接入告警之后,像“自动化任务挂了三天”这类静默故障才终于能第一时间发现。
7. 从项目到产品:我学到的最重要的几件事
写到这里,DeskcommCRM 已经上线运行了十个月。我不敢说这套系统已经完美,但它确确实实让销售团队的工作效率提升了一个台阶,也让管理层第一次能看到“漏斗每一层到底卡在哪”。复盘整个过程,有几件事对我个人来说特别有启发。
第一件事,自研系统最怕的不是技术难,而是需求蔓延。立项的时候我们划定了边界,但在开发过程中,销售主管几乎每两周就会提出一个“加个小功能”的需求。我们定了两条拦截规则:使用频率低于每周一次的暂时不做,能通过现有配置实现的不另起炉灶。靠着这两条规则,我们挡掉了至少十个看似合理但不重要的需求,在第四个月按时完成了核心模块。
第二件事,数据模型比接口 API 重要得多。接口设计错了可以快速改,前端页面错了可以推翻重写,但表结构一旦上线,数据迁移的成本是指数级的。我们前期花在数据模型设计上的时间大概有 20 个工作日,包括反复跟业务主管确认“公司”和“联系人”的边界、确认“商机阶段是否允许跳转”等细节。现在回头看,这些讨论的时间一点都没白费。
第三件事,自研 CRM 是一个持续投入的过程,不是一锤子买卖。上线之后,我们每个月至少还会花 3-5 天维护和迭代。新需求的优先级不是我们研发团队说了算,而是业务主管带着数据来申请,说得清楚业务价值,排期评审才会放行。
如果你正在考虑自己动手做一套 CRM,我的建议是:先把“不做清单”写出来,然后再写“要做清单”。想清楚边界之后,你会发现这套系统的功能和架构都会清晰很多。DeskcommCRM 对我们来说远远不只是一个软件,它已经变成了理解业务、推动业务的一种方式。