ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

自研CRM系统实战:从数据模型到权限设计的完整拆解

自研CRM系统实战:从数据模型到权限设计的完整拆解 进入CRM这个领域说起来有点偶然。之前我们团队一直用的是某款通用CRM功能确实全面但越用越别扭——销售、客服、实施三个部门各自为政客户信息在不同系统里来回倒腾数据口径经常对不上。后来客户投诉多了老板一拍桌子说干脆自己搞一套。于是就有了 DeskcommCRM 这个项目从立项到落地差不多折腾了半年。这篇文章把我的完整思路、技术选型、踩坑经历都整理出来给准备自研或深度定制CRM的团队做个参考。1. 立项之初DeskcommCRM要解决的真实问题1.1 业务痛点在哪里先说说我们当时面对的烂摊子。公司业务线分三块一是标准产品销售二是定制化项目实施三是售后维护服务。这三条线的客户信息是重叠的但管理方式完全割裂——销售记录在A系统实施过程用Excel跟踪售后问题散落在群里和邮件里。最要命的是客户交接。销售离职或者内部换人跟进的时候新接手的人根本不知道之前聊到哪里了合同条款、客情关系、承诺过的交付时间全靠猜。还有更头疼的同一个客户可能同时在下单、在实施、在报修但三个角色互相看不到对方的进展导致销售承诺了实施排期根本排不进来客服答应的问题实施侧压根不知道客户体验自然一塌糊涂。所以我立项时定的第一个原则是DeskcommCRM不是要把所有流程都管起来而是要先把客户全生命周期这条主线打通。客户从线索到成交、到交付、到售后所有关键节点的信息必须在一个系统里看得到。1.2 为什么不自研一套而是改造开源方案可能有人会说你们需求这么明确直接买个现成的CRM不就行了为什么非要自己搞我当时算了一笔账。市面上的商业化CRM按坐席收费我们50个用户一年下来订阅费小几十万而且定制接口还要额外加钱。更重要的是很多CRM的核心数据模型是锁死的——比如客户字段、跟进状态、商机阶段想改就得提需求单排队等排期。再看开源方案像SuiteCRM、EspoCRM、Odoo的CRM模块功能底座是够用的但二次开发的学习成本和改造工程量也不能小觑。我们最终选了一条中间路线以开源CRM为底座把核心业务逻辑重写外壳重新设计对外统一叫DeskcommCRM。说白了借用开源项目的成熟骨架避免从零开始造轮子但核心能力全部按自己的业务逻辑来定制。1.3 项目边界与核心目标确定立项会上我提了三个核心目标后面所有工作都围绕这三个目标展开统一客户视图全公司任何角色打开一个客户页面能看到所有相关信息。可配置的销售流程不同产品线、不同金额等级的商机流程可以自定义而不是一套流程走到底。内外部协作留痕所有沟通记录、任务分配、客户反馈必须沉淀在系统里避免信息停留在个人微信和邮件里。同时我明确划了一条线——我们不做财务模块不做ERP集成。CRM就是CRM管好客户相关的数据流转就是本分把手伸太长反而会拖慢项目节奏。这个边界意识在后面帮了大忙几次需求蔓延都被这条红线挡住了。2. 架构设计与技术选型背后的取舍2.1 技术栈选择DeskcommCRM的技术栈我纠结了很久。原先团队擅长的是PHP但考虑到后续要做实时消息推送和复杂权限控制我最终选择了Node.js React的组合。后端用的是NestJSTypeScript带来的类型安全在后期的迭代中省了非常大的精力。前端用React Ant Design ProAnt Design的组件体系做后台管理系统真的很顺手表格、表单、弹窗这些高频组件开箱即用省了大量UI开发时间。数据库选型上主库用了PostgreSQL主要看重它在复杂查询上的表现。CRM系统大量的客户画像查询是动态字段的PostgreSQL的JSONB类型可以很好的应对。比如客户的个性化属性字段随时可能要加一个“预计采购量”之类的自定义列JSONB直接存不需要频繁改表结构。缓存用了Redis主要用来做会话管理和热点数据缓存。有一段时间客户列表页很慢排查下来发现每次打开列表页都去查一次全表统计后来把这个统计数据在Redis里做5分钟缓存响应时间从2.8秒降到了400毫秒左右。2.2 数据模型设计数据模型是整个CRM的骨架这块我花的时间最多。核心数据模型有五个客户Account、联系人Contact、商机Opportunity、跟进记录Activity、工单Ticket。这里说几个关键设计。客户与所有业务对象的关联用了剥洋葱式的层级关系。客户下面有联系人联系人有沟通记录客户有商机商机有跟进日志客户又有工单工单涉及产品、解决方案。任何一张主表都能关联到客户并且通过客户ID拉起这个客户的全景视图。客户全景视图的SQL关联结构大致是这样async getCustomerOverview(customerId: string) { const [account, contacts, opportunities, tickets, activities] await Promise.all([ this.accountRepo.findOne({ where: { id: customerId } }), this.contactRepo.find({ where: { accountId: customerId } }), this.opportunityRepo.find({ where: { accountId: customerId }, order: { updatedAt: DESC } }), this.ticketRepo.find({ where: { accountId: customerId }, order: { updatedAt: DESC } }), this.activityRepo.find({ where: { accountId: customerId }, order: { createdAt: DESC }, take: 20 }), ]); return { account, contacts, opportunities, tickets, recentActivities: activities }; }自定义字段是另一个重点。不同业务线对客户字段的需求不同销售关注采购决策链实施关注技术接口对接人售后关注服务级别。我在客户表上预留了一个JSONB字段extra_attributes并且做了一个字段配置表让管理员可以自行配置表单上的自定义字段映射到这个JSONB上。2.3 权限体系设计权限是整个CRM里业务价值最高也最容易做砸的地方。我们一开始就定了基调数据权限要做到行级控制而不是页面级控制。比如销售A和销售B都能看到同一个客户的页面但A只能看到自己创建的商机B只能看到自己负责的工单。共享客户的时候可以设置共享人的查看权限是只读还是可编辑。实现行级权限用的是PostgreSQL的Row Level Security在数据库层面就做了行过滤而不是纯粹依赖应用层过滤。这样无论如何通过API查询数据都是安全的不会因为某个查询接口忘了加where条件就导致越权。一段RLS策略示例CREATE POLICY sales_opportunity_access ON opportunity USING ( owner_id current_setting(app.current_user_id)::uuid OR id IN ( SELECT opportunity_id FROM opportunity_share WHERE shared_with_id current_setting(app.current_user_id)::uuid ) );有了这层策略应用层完全不需要重复写条件代码简洁了很多而且权限逻辑集中管理不容易漏。3. 核心功能模块的落地细节3.1 客户信息管理的设计思路客户信息管理这个模块看起来简单但做起来细节非常多。首先是客户查重。业务员录新客户的时候如果系统里已经有这个公司了再录一遍就是数据垃圾。我们做了企业名称的相似度匹配用的算法是编辑距离加拼音首字母组合匹配。查重逻辑大致是async findDuplicateAccounts(companyName: string) { const normalized this.normalizeCompanyName(companyName); const candidates await this.accountRepo.createQueryBuilder(account) .where(similarity(account.name, :name) 0.6, { name: normalized }) .orWhere(account.name_pinyin :pinyin, { pinyin: this.toPinyinAbbr(normalized) }) .getMany(); return candidates; }这里用到了PostgreSQL的pg_trgm扩展的similarity函数效果还行。但实际操作中业务员有时候还是会录重复因为客户登记的时候用的公司名和工商系统注册名不一样这个只能靠运营制度去约束不能完全依赖系统。客户分群和标签体系也是这个模块的重要组成部分。每个客户可以打多个标签比如“超大型客户”“季度回款”“需求紧急”等。标签存储在客户表的一个tag_ids数组字段里通过标签筛选客户时直接用数组包含操作性能非常好。我们后期还加了客户动态评分综合客户活跃度、商机金额、跟进频次、最近联系时间等维度算出一个0到100的分数分数高的客户自动置顶业务员打开系统的第一眼就能知道今天该联系谁。3.2 销售漏斗与商机阶段管理销售的日常工作围绕商机展开一个商机代表一个潜在的成交机会。商机阶段我们按照业务习惯设计了初识、需求分析、方案报价、商务谈判、赢单、输单六个阶段每个阶段可以配置必填字段。比如从“初识”推进到“需求分析”必须填写客户预算、决策人、需求时间表从“方案报价”推进到“商务谈判”必须上传报价单和竞品信息。这些必填字段的作用是防止销售为了冲业绩虚假推进也是数据质量的最后一道防线。销售漏斗分析页面用ECharts做了漏斗图const funnelData stages.map(s ({ name: s.name, value: s.opportunityCount, }));同时统计每个阶段的转化率和平均停留时间。有一次数据分析发现从“需求分析”到“方案报价”的转化率只有38%远低于行业平均水平。后来逐个销售访谈才发现问题出在方案产出太慢技术顾问忙不过来客户等不及就流失了。后来专门开发了方案模板库销售和技术顾问可以基于模板快速拼装方案转化率提升到了57%。3.3 工单协作与自动提醒工单模块是DeskcommCRM里我最得意的一块。它管的是客户购买后的服务请求比如上门安装、远程调试、故障排查、定期巡检等。工单从创建到闭环涉及的协作方很多——客服接待、技术派单、工程师接单、客户确认任何一个环节卡住都会影响客户体验。工单状态我设计了待受理、处理中、待客户确认、已关闭四个大状态加一个已取消的终态。每个状态流转时都有通知规则比如工单创建后10分钟没有受理自动提醒客服主管处理中超24小时没有更新自动提醒项目经理客户超过48小时不确认关闭自动提醒跟进人回访。自动提醒的实现用了一个定时任务加Redis延时队列。工单状态变更时往Redis里塞一条带延迟执行时间的任务时间到了之后检查工单状态是否满足提醒条件满足就推通知。伪代码大概长这样async scheduleReminder(ticketId: string, delaySeconds: number, reminderType: ReminderType) { const reminderKey reminder:${ticketId}:${reminderType}; await this.redis.set(reminderKey, JSON.stringify({ ticketId, reminderType }), EX, delaySeconds 60); // 延迟任务通过Bull队列触发 await this.reminderQueue.add( { ticketId, reminderType }, { delay: delaySeconds * 1000 } ); }这个设计上线后客户对售后响应速度的投诉明显减少。关键不是提醒本身有多先进而是有了它之后整个服务团队不敢随意丢单了每张工单都有人在盯。3.4 数据看板与报表数据看板是管理层用最多的模块。我做了几个标准看板销售业绩看板、商机分布看板、工单时效看板、客户健康度看板。每个看板支持按时间、部门、产品线维度筛选。这里有个教训报表需求开始用实时查询实现结果后台数据库压力很大。后来我把报表数据改成了定时物化视图每15分钟刷新一次。数据会滞后最多15分钟但对于管理层看周报月报来说完全够用数据库压力降了80%。物化视图刷新用pg_cron定时任务CREATE MATERIALIZED VIEW sales_dashboard AS SELECT date_trunc(day, created_at) AS day, owner_id, COUNT(*) FILTER (WHERE status won) AS won_count, SUM(amount) FILTER (WHERE status won) AS won_amount, COUNT(*) FILTER (WHERE status open) AS open_count FROM opportunity GROUP BY 1, 2; REFRESH MATERIALIZED VIEW CONCURRENTLY sales_dashboard;4. 实施过程中踩过的坑4.1 数据迁移的意外困境从旧系统切到DeskcommCRM数据迁移是第一道坎。旧系统的客户数据存在不同的Excel里格式五花八门字段命名也完全不统一。有一次迁移联系人表发现同一个联系人手机号有的加了86前缀有的没加导致匹配不上。后来写了一段清洗脚本统一格式才把数据质量提上来。迁移完成后我做了随机抽样比对发现还是有3%左右的客户数据因为缺少必填字段没有迁入。这些数据后来打包成CSV发给各业务线负责人人工确认后补录。这个过程很痛苦但也是逼着各业务线梳理自己客户资产的一次机会。4.2 权限越权问题权限越权的问题是在内测阶段暴露的。测试人员发现一个普通销售可以通过修改URL中的参数查看其他销售的客户详细页。原因是最初我在应用层做权限过滤但有些查询接口忽略了过滤条件。后来我改成数据库RLS策略彻底解决了这个问题。但这带来一个新的问题——某些场景下系统管理员需要查看所有数据RLS策略会拦截管理员的查询。解决办法是给管理员角色设置一个特殊的current_setting值RLS策略里放行。这个坑给我的教训是权限设计一定要在最底层做靠应用层“记得”加条件是靠不住的总有漏网之鱼。4.3 性能瓶颈列表页为什么越用越慢系统上线一个月后客户列表页变得越来越慢最慢的时候要5秒多才能打开。排查发现列表页用了Count(*)做总数统计而客户表数据量已经超过30万行每次打开页面都要全表扫描。优化方案有两条一是把总数统计改为估算值PostgreSQL的EXPLAIN可以直接出估算结果不精确但足够用二是给列表页加Redis缓存5分钟过期。两个方案叠加页面响应时间降到了500毫秒以内。列表页分页还有个细节数据量大后LIMIT/OFFSET翻页越翻越慢。我把分页改成了基于游标的分页方式前端传上一页最后一个客户的ID作为查询起点性能稳定不会因为翻到后面几页就变慢。这个改动在商机列表和工单列表同样适用。5. 上线后的迭代与运营经验5.1 推广落地比开发更难系统开发完成只是第一步真正的考验是让团队用起来。我原本天真地以为只要系统好用大家自然会用。现实是销售们已经习惯了Excel和微信让他们每天登录系统录入跟进记录阻力非常大。我后来改变策略不是强制命令而是先找几个配合度高的业务骨干做种子用户把他们的使用体验打磨顺滑。比如销售最烦的是手动录跟进记录我就开发了自动记电话的功能——销售通过系统内置的呼叫组件打电话通话结束自动生成一条跟进记录销售只需要补充总结即可。这个功能上线后跟进记录的录入率飙升了4倍因为省了太多事。5.2 数据驱动的复盘机制系统运行稳定后我开始每周给管理层发一份数据周报内容包括新增客户数、商机转化漏斗、工单平均处理时长、客户健康度评分分布等。有了数据管理层开始主动关心系统里的信息是否准确进而倒逼业务线认真维护数据。有一份数据让我印象很深刻客户健康度评分低于30分的客户中有47%在过去90天内没有任何跟进记录。这些客户如果一直没人管流失几乎是必然的。我们就此建立了客户挽救机制健康度评分低于阈值的客户自动生成待办任务推送给对应的客户经理。5.3 持续迭代的方向DeskcommCRM上线半年后我列了一个后续迭代的路线图移动端适配销售在外跑客户时手机端打开频率远高于PC端目前只做了H5轻量版后续考虑做个React Native原生壳。AI辅助跟进建议基于历史成功商机的跟进模式给销售推荐最佳联系时间和沟通话术。客户门户让客户通过微信小程序自主提工单、查进度、看历史服务记录减少客服转述环节。报表自定义能力用户能自己拖拽字段生成报表不用每次需求都找研发。6. 一点心得如果让我重新做一次DeskcommCRM我会在开工前花更多时间和销售、客服、现场工程师坐在一起听他们讲讲一天的工作流而不是只看管理者眼中的流程。系统最终是给一线人员用的他们对效率的感知最敏锐也最能提供有价值的改进方向。另外技术选型别盲目追新。这次用PostgreSQL的RLS、JSONB、物化视图都是成熟稳定的功能没有追求花哨的中间件整个系统的运维负担因此低了很多。自研CRM本质是业务工程不是技术炫技。把业务逻辑理清楚、权限安全做扎实、性能瓶颈提前预防比用什么框架重要得多。希望这篇拆解能够给还在CRM选型和自研路上纠结的团队一些参考。
返回列表