ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零搭建DeskcommCRM:动态字段、状态机与权限设计实战

从零搭建DeskcommCRM:动态字段、状态机与权限设计实战 1. 为什么我要自己搭一套 DeskcommCRM而不是直接买现成的聊到 CRM很多人第一反应是“市面上那么多成熟的系统直接用不就行了”。这话放在两年前我也认同但当你真正在业务一线用过几家之后会得出一个扎心的结论工具是给流程服务的不是让流程迁就工具的。我们团队做的是 B 端客户运维与销售线索管理业务链条里有大量非标字段、自定义状态流转和跨部门协同需求。拿通用型 SaaS CRM 跑起来要么是字段不够灵活要么是权限模型对不上组织架构更麻烦的是数据始终存在别人服务器上后续要做二次开发或者接内部 BI 报表处处碰壁。所以我才决定自己动手从零搭一套贴合自身业务的 DeskcommCRM。这不是要重新发明轮子而是要把轮子做成适合自己车子的形状。项目核心目标很明确一是完全掌控数据结构字段、状态、关联关系都能按业务动态调整二是把销售全流程——从线索导入、客户分配、跟进记录、商机推进到合同回款——沉淀成一套清晰可追踪的数据链路三是给管理层提供实时看板减少每周手工汇总的重复劳动。这套系统上线后最直观的变化是销售团队录单效率提高了不少原来在表格和聊天记录里来回翻找客户信息的情况基本消失了。如果你也在犹豫是选型采购还是自己开发这篇文章里我踩过的坑和沉淀下来的设计思路应该能帮你少走不少弯路。适合的人群主要是那些有一定开发能力、业务逻辑相对复杂、又不想被现成产品绑死的中小团队。2. DeskcommCRM 的整体设计与核心模块拆解2.1 整体架构前端轻量化后端重逻辑在定技术方案之前我先梳理了团队的实际使用场景。销售端可能需要在地铁里、客户现场打开页面快速查资料所以前端要轻、要快后端则要扛得住几万条客户记录和复杂的关联查询。最终我选用了经典的 React Node.js PostgreSQL 组合。前端用 React 搭配 Ant Design 组件库最大好处是表格、表单、日期选择这类 CRM 高频组件可以直接复用开发速度非常快。后端用 Node.js 的 NestJS 框架它自带的模块化结构和依赖注入机制很适合 CRM 这种业务模块多、层级深的系统。数据库选 PostgreSQL 而非 MySQL是因为它原生支持 JSONB 字段和数组类型这对自定义字段的设计简直是福音——用户想加几个自定义属性不需要频繁跑 ALTER TABLE。整体架构上我把系统拆成六个模块客户管理、线索管理、跟进记录、商机管理、合同与回款、统计看板。每个模块保持独立模块之间只通过服务层调用不直接操作对方的数据库表这样后期改任何一块都不至于牵一发动全身。2.2 客户管理的核心设计动态字段与标签双引擎客户表是所有业务的地基但“客户”在不同团队眼中内容完全不同。销售关注联系人、规模、决策链售后关注合同编号、服务等级、到期时间财务关注账期和信用额度。如果只建一张固定字段的表必然会有大量冗余或者缺失。这里我采用了“基础字段 动态扩展”的双层设计。基础字段是那些所有客户都必需的属性比如公司名、行业、来源渠道、负责人动态扩展则存在一个 JSONB 字段里前端动态渲染表单。这样既保证了核心查询的高效又留足了弹性空间。比如一个做政府项目的客户销售可以在动态字段里加“招标项目编号”“预算金额”而这些字段不会出现在普通企业客户页面上。标签系统则是给客户打多维度的标记比如“高意向”“需要回访”“VIP 客户”“风险客户”。标签和动态字段的区别在于标签是跨维度、可叠加的用于快速筛选和自动化流程触发。我特意在客户表里建了tags数组字段配合 PostgreSQL 的 GIN 索引即便是几万条客户里按标签组合查询响应也在毫秒级。2.3 线索到商机的流转逻辑状态机比任意改状态可靠得多CRM 系统里最容易出乱子的就是状态管理。刚上线的时候我天真地允许用户通过下拉框任意修改线索状态结果一周下来数据就变得没法看了——有人把已成交的客户改回了“待沟通”有人把已经弃单的线索重新激活却没人记得补跟进信息。后来我重新设计了状态机严格定义线索的五种状态新导入、跟进中、已转商机、已关闭、已废弃。每条状态流转只能按规则进行比如只有“跟进中”的线索才能转为商机只有“商机”阶段才能标记成交转“已关闭”必须填写关闭原因。状态机逻辑我放在了后端服务层统一校验前端只传意图不直接改状态这就从源头堵住了脏数据。实际跑下来这种克制反而让销售更规范了。状态变化本身成了真实数据管理者查看漏斗时每个环节的转化率都靠谱得多。回头看状态机定义这一步其实是整个系统数据质量的生命线比多写几个页面有价值得多。3. 核心实现细节与关键代码解读3.1 动态表单渲染的实现思路动态字段的难点不在存储而在前端渲染和后端校验的配套设计。我先在数据库里维护一张custom_field_config表记录字段名、中文标题、类型、是否必填、可选项然后前端组件根据这个配置自动渲染表单。核心渲染逻辑我封装成一个DynamicForm组件接收字段配置数组遍历生成对应的表单控件。文本、下拉、日期、多选这些类型做一个映射关系配置里有type字段前端判断类型渲染不同的控件即可。这样新增字段时后端配置一条记录前端不用改代码表单自动多出一个输入项。后端校验同样依赖这份配置。NestJS 里我用自定义 Pipe 做校验循环读取配置必填项为空则报错下拉选项不在备选集合则报错。这个方案的妙处在于业务规则和代码逻辑解耦——业务人员自己就能通过后台界面加字段不用每次提需求等排期。3.2 跟进记录的防重复与时间线展示跟进记录是销售的“工作细胞”但也是最容易被敷衍填报的部分。我在设计时加了两道硬约束一是同一客户同一时间段内不允许重复添加跟进记录防止销售像打卡一样刷数据二是跟进内容最小字数校验少于十个字会自动提示逼着销售至少写清事实。时间线展示我采用了“单表 冗余摘要”的做法。一张follow_up_record表存所有记录但列表页只取当前客户最关键的摘要字段比如跟进方式、下一步计划、下次跟进时间。要做按客户聚合的时间线视图时直接按客户 ID 做一次查询时间倒序返回压力不大逻辑也直观。这里要特别提一下“下次跟进时间”这个字段的设计。它存在跟进记录表里但会自动同步到客户主表的next_follow_up_at字段。这样销售首页可以快速列出“今天需要跟进的客户”其实就是查主表字段而不是翻记录表。同步逻辑在写入跟进记录时事务处理确保两张表永远一致。3.3 数据权限控制谁可以看什么必须提前想清楚权限控制是 CRM 里最容易埋雷的地方。我一开始图省事只做了角色级别控制分了管理员、销售主管、普通销售结果上线第三天就出问题了——两个销售小组的负责人能看到对方组员的所有客户数据。后来我调整为“角色 数据范围”双层模型。角色决定能操作什么功能数据范围决定能看到哪些人的数据。数据范围有四种粒度仅本人、本部门、本部门及下属部门、全公司。实现上我在每次客户查询时动态拼接 SQL 条件根据当前登录用户的部门和角色生成 WHERE 子句而不是在业务代码散落各种 if-else。权限这块某种程度上比业务功能还重要因为 CRM 里装的是公司最核心的资产——客户关系。权限模型一旦定错后期改造成本极高所以建议在做任何界面之前先把数据范围矩阵画出来和自己团队逐条确认。3.4 统计看板的数据实时性策略看板不需要真正的毫秒级实时。最开始我图新鲜用了定时任务每五分钟刷新一次数据后来发现大部分指标根本不需要这么高的频率。现在我改成三层数据体系实时查询用于单客户详情、跟进记录等 OLTP 场景定时汇总用于首页漏斗、销售排行等高频看板每半小时跑一次汇总任务结果存汇总表每日快照用于管理层日报、趋势分析每日凌晨生成当日各维度快照这种分层的意义在于避免为了展示数据而频繁扫全表也不会出现管理层想查昨天的数据却只能看到一个粗略的今日值。汇总任务用 Node.js 的定时任务框架 node-cron 实现逻辑简单可靠。4. 部署环境的搭建与性能优化实战4.1 服务器选型与基础环境配置项目上线后的性能瓶颈通常不在代码而在部署架构是否合理。我用的是单台 Linux 服务器 Docker 容器化部署数据库和应用都跑在容器里但数据目录挂载到宿主机磁盘。这样做的好处是迁移方便服务器挂了在另一台机器上直接拉起容器就能恢复。服务器配置上建议至少 4 核 8G 起步因为 PostgreSQL 比较吃内存Node.js 应用本身倒是很轻。如果团队预算紧张在数据量达到几十万级之前这一台机器足够支撑了。Nginx 放在最外层做反向代理同时处理 HTTPS 证书和静态资源的缓存。一个实操细节Docker 的日志文件默认无限增长如果不管几个月后可能把磁盘塞满。我在 docker-compose 配置里给每个容器加了logging选项设置单文件 50M、保留三个文件这算是个基础但容易忽略的坑。4.2 数据库索引设计的三个关键点PostgreSQL 的性能和索引设计直接相关说三个我实际验证过有效的点第一外键字段必须建索引。客户表的owner_id、company_id跟进记录表的customer_id、creator_id凡是建立关联的字段全部建普通 B-tree 索引。第二查询频繁的状态字段和标签字段用部分索引配合条件查询效果更佳。第三JSONB 动态字段里如果经常做条件筛选给特定键建 GIN 索引而不是整列做索引节省存储空间。我分享一个实际案例。原来线索列表页按来源渠道筛选数据到两万条时查询要 1.8 秒加了source_channel索引之后降到 80 毫秒体验完全不同。索引这种东西不用刻意追求全量覆盖但核心查询路径一定要提前设计。4.3 Docker Compose 编排与备份策略整套服务我用 Docker Compose 编排核心服务有前端 Nginx 容器、Node.js 后端容器、PostgreSQL 数据库容器。Compose 文件里定义好环境变量比如数据库连接串、JWT 密钥、文件存储路径全部放到.env文件统一管理避免密钥硬编码到代码仓库。备份策略我采用了双保险一是数据库容器里定时执行pg_dump全量备份到宿主机二是用 rsync 把宿主机备份目录同步到另一台内网机器。老话说得好备份这件事不做就是赌博做了才叫负责。我甚至遇到过一次磁盘意外损坏的情况幸好备份策略提前设好数据恢复只花了半小时。加密和权限方面PostgreSQL 默认只监听本地地址我在 Compose 里映射端口时特意没有暴露到公网只让后端容器通过内部网络连接这样即使应用层出现漏洞数据库也不会直接暴露。5. 常见问题与排查技巧实录5.1 连接数耗尽导致接口超时系统上线两个月后遇到一次所有接口变慢的故障排查下来发现是 PostgreSQL 的连接数被占满了。原因是 NestJS 的 TypeORM 连接池配置没调好默认连接池上限远超数据库能承受的范围。解决办法是把 TypeORM 的pool参数调到和数据库max_connections匹配同时给代码里所有数据库操作确保释放了连接。这个故障最值得分享的一点在于不要盲目提升连接池上限。连接数不是越多越好每个连接消耗内存过多的并发连接反而会导致数据库上下文切换频繁性能下降。5.2 动态字段配置错误导致前端白屏动态表单确实方便但也遇到过自己坑自己的情况。有一次我在字段配置里设置了一个类型为“多选”却忘了配置可选项前端拿到空数组后渲染多选组件报错整页直接白屏。后来我在前端加了兜底逻辑遇到非法配置直接跳过该字段并给出警告而不是让整个页面崩溃。从这个案例延伸出来的经验是动态化虽然灵活但必须做好配置校验和容错处理。前端兜底、后端校验、配置管理端预览三道防线缺一不可。5.3 看板数据与列表页对不上销售主管反馈看板显示的已成交客户数量和客户列表页筛选出来的数量不一样。排查后发现是两个统计逻辑用的过滤条件不一致看板统计的是“商机状态为成交”列表页筛选的是“客户状态为成交”而这两个状态不一定同时更新。这本质上是一个定义一致性的问题。我看到这个问题时的第一反应是不能只修一个界面要统一状态定义。于是把所有统计口径都收敛到一个服务模块中统一取数逻辑前端只传参数不碰业务过滤条件。这之后数据不一致的问题彻底消失。多做一步检查和验证比一次接一次地修修补补强得多。这也是我整个项目下来最深的一点体会数据字典、状态定义、指标口径这些看似琐碎的事情才决定了系统能不能让人放心地长期使用。6. 一些维护层面的建议系统上线仅是开始后续还有持续的迭代。我有几个建议给正在做同类项目的人。第一版本控制一定要从第一天就做好。包括数据库结构变更最好引入 Migration 机制。最开始手动改表结构时间久了环境之间差异越来越大部署时总会出幺蛾子。后来我切换到规范的 Migration 流程每次变更生成迁移文件代码和数据库结构同步管理环境一致性好很多。第二给关键接口加日志。尤其是登录、权限校验、数据导出这类涉及敏感信息的操作必须有完整的操作日志不仅为了排查问题更是为了厘清责任边界。我用的方案是 NestJS 拦截器全局记录请求路径、操作人、耗时和执行结果攒了足够的日志后也能用于分析用户行为、优化系统性能。第三不要怕做减法。用户提了一百个需求不代表一百个都要做。业务方更在意的是系统能不能让他们少点几下、少录几次、少走两步。功能做到精而不是多数据准确比功能丰富更能提升团队使用意愿。这也是我做完 DeskcommCRM 之后最想强调的项目心法。从最开始的一个想法到现在团队每天都在用的核心工具这套系统给我的最大收获不是技术上的锤炼而是一种认知上的更新好工具是长在业务里的它的最高标准不是炫酷而是顺手。如果你也在做类似的内部系统希望这篇内容对你有所启发也欢迎在实践中根据你的团队节奏不断调整做出真正合身的系统。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进