DeskcommCRM实战复盘:从数据建模到消息推送的完整落地指南

发布时间:2026/10/19 5:11:21
DeskcommCRM实战复盘:从数据建模到消息推送的完整落地指南 最近在复盘DeskcommCRM这个项目感触挺多。如果你也是做SaaS产品、企业内部系统或者刚接手一套CRM系统的研发、运维那这篇内容应该能帮上忙。DeskcommCRM说白了就是一套把客户信息、销售跟进、工单支撑和桌面通信通知串起来的管理系统核心解决的是“客户数据散落在Excel、IM聊天记录和个人手机里”这种普遍问题。这套系统从零搭到上线横跨了需求梳理、技术选型、数据建模、模块开发、数据迁移这几个阶段中间踩了不少坑也沉淀了一些真正能复用的经验。下面我会把整个项目的设计思路、核心模块的实际落地过程、关键表结构怎么设计、还有遇到底层数据冲突、通知丢失、慢查询时怎么排查全部摊开讲。无论你是准备自研CRM还是在已有业务系统里补一个客户管理模块这篇文章都给你一条能直接照着走的路。1. 项目定位到底要做一个什么样的CRM1.1 为什么叫DeskcommCRM命名的由来和含义先解释一下项目名。Deskcomm是“Desk Communication”的缩写当初起这个名字是想强调这套系统不只是传统意义上的CRM——它不只管销售管道和客户资料还要把“桌面端沟通”这个能力做进去。很多人一看CRM就以为是Salesforce那种重销售流程的系统但实际在中小团队里CRM的边界往往模糊得多。我们当时面对的真实场景是售前人员在电脑前处理客户咨询需要一边查客户历史购买记录一边回复IM消息还要顺手创建服务工单。如果这三件事分别在三个系统里做效率极低而且信息是断裂的。所以DeskcommCRM从一开始就定了两条核心主线一是把客户全生命周期数据统一管理二是把客户互动行为包括工单、通话、在线消息自动关联到客户档案上。它的定位不是“销售管理软件”而是“面向客户接触场景的工作台”。所以如果你也想自研CRM我第一个建议是先别急着画功能列表先明确你的CRM是给谁用、解决哪个环节的断裂。是给销售管理Pipeline还是给客服做工单和客户360°视图还是给市场做线索孵化定位不同数据模型设计会差非常多。1.2 核心需求拆解与业务流程梳理明确定位后我们花了两周时间梳理业务流程最终把需求收敛为以下五个核心功能域客户信息管理包括企业客户和个人联系人两类对象支持自定义字段比如客户规模、所属行业、产品使用版本、到期时间、客户来源等。线索管理市场投放和商务拓展带来的原始线索先进入线索池经过初步筛选和分配转成正式客户避免线索堆积和撞单。跟进记录销售和客服每次与客户接触都要求留下文字跟进记录并支持关联下次跟进时间系统自动提醒。工单支撑客户报障、需求反馈统一走工单流程状态流转包括待处理、处理中、待客户确认、已关闭工单需要能直接关联到客户档案和跟进记录。桌面通知与待办中心所有分配给当前用户的待办事项、工单流转、到期提醒通过桌面端WebSocket推送到浏览器通知中心不用反复刷新页面。业务主流程串起来是这样的线索进入线索池分配后销售跟进并转成客户客户购买后产生工单需求客服在客户详情的工单页直接创建工单处理过程中写的跟进记录自动挂在客户时间线上整个过程中每一次状态变更都会触发桌面通知提醒相关责任人。从这个梳理过程看最花时间的不是功能设计而是和业务方对齐“客户”的定义。后来我们索性在系统里增加了一套“客户唯一性判定”规则比如统一社会信用代码优先没有则用名称规范化后匹配再不行就靠人工合并。这个规则在第三部分的数据建模里会详细说。2. 技术选型与数据模型设计2.1 技术栈选择的三个理由DeskcommCRM的技术栈是在纠结了很久之后定的最终方案是前端使用Vue 3 TypeScript Element Plus后端使用Java Spring Boot 3.x数据库使用PostgreSQL缓存和消息推送中间件分别使用Redis和WebSocket网关部署采用Docker Compose。为什么这么选核心原因有三条。第一团队对Java和Vue最熟悉成熟稳定比技术新颖更重要CRM系统里全是业务逻辑没有多少高并发场景不需要为了炫技引入新框架。第二PostgreSQL在JSON字段支持上比MySQL更好而CRM系统最大的特点就是自定义字段多——客户要加一个“下次回访时间”你不可能每次都改表结构所以大量用JSONB存扩展属性。PostgreSQL的JSONB可以建GIN索引查询过滤非常方便。第三Spring Boot生态对定时任务、消息、WebSocket的支持非常完善Spring的Scheduled做到期提醒WebSocket做桌面推送几乎不用额外引入中间件运维成本低。这里想说一个经验CRM系统的瓶颈通常不在单表千万级数据这种量级而在于筛选条件复杂。客户列表页往往要同时过滤行业、地域、客户来源、最近跟进时间、标签类型这时候一张客户表加多张关联表配合好索引和JSONB字段基本就够用了。没必要一开始就上Elasticsearch或用ClickHouse做宽表真到了几千个自定义字段、上万用户同时在线再考虑也不迟。2.2 客户主数据与线索池怎么建表数据模型是整套系统的地基这一块我们设计得最久也踩得最痛。先说客户主表核心设计是“宽表 JSONB”基础字段id、customer_name、customer_type企业/个人/其他、industry、owner_user_id、source、status、created_at、updated_at。统一社会信用代码credit_code做唯一约束用允许为空为空时由外部规则处理。扩展字段extra_attributesJSONB类型存对外电话、员工规模、产品版本等信息。软删除deleted_at所有业务表都加这个字段避免误删后数据无法恢复。线索池则是另一张线索表字段类似客户表但多一个status字段标识“新线索、已分配、已转化、已废弃”以及一个assign_to表示分配给了谁。线索池和客户表之间通过customer_id字段建立弱关联线索转化时不直接删除线索而是把状态置为“已转化”好处是后续还能溯源这个线索是怎么来的。关于建表给一个具体建议唯一性约束不要只放在数据库层也要在代码层做并发防重。两个销售同时录同一个客户如果数据库层只有credit_code唯一约束而credit_code为空那就会出现重复数据。我们后来在PostgreSQL里做了一个unique index用“COALESCE(credit_code, ) || customer_name”作为索引字段虽然过滤条件里要处理空串但至少保证了最基本的不重复。2.3 权限模型部门、角色、数据范围怎么设计CRM系统里权限设计翻车的概率极高。我们一开始参照网上教程做了RBAC用户-角色-权限做到一半发现根本不够用。因为CRM的核心诉求不只是“谁能访问这个按钮”而是“谁能看到哪些客户数据”。最终我们用RBAC 数据范围组合的方案。所谓数据范围就是四条规则本人数据销售只能看到自己名下的客户管理员和老板可以看到所有人。部门数据部门经理可以看到本部门所有人的客户。下级数据存在团队层级时上级可以看到其所有直接和间接下级名下的客户。自定义共享通过共享规则把某个客户临时共享给指定用户或部门带有效期。实现方式上用户在某个模块请求数据时后端根据当前用户角色查出允许的数据范围再拼到查询条件里。这里有个坑是不要在SQL里到处加“owner_user_id currentUserId”这种逻辑而是提前构造一个“可见客户ID集合”用IN约束。虽然性能上IN的数量可能很大但配合分页和索引实际场景完全可以接受。如果数据量大到百万级再考虑建权限中间表。3. 核心模块落地从客户画像到跟进闭环3.1 客户画像与标签体系的实现客户画像不是搞得很玄乎什么算法画像对CRM系统来说画像就是把零散数据汇总成一个页面客户基础信息、联系人列表、历史跟进记录、工单列表、付款记录、最近互动时间、标签列表、关联文件。DeskcommCRM的客户详情页就是这样一个聚合页底表是客户主表左侧Tab展示关联数据。标签体系这块我们选择了PostgreSQL的数组字段实现而不是单独的标签表。客户表加一个tag_array text[]字段用GIN索引支持包含查询。为什么不用关联表因为标签使用场景以“给客户打标签”和“按标签筛选客户”为主很少做标签的独立管理数组字段一条SQL就能实现不需要JOIN性能反而更好。当然如果你要统计每个标签的客户数那标签表是更好的选择这个要看业务侧重。3.2 跟进记录与销售漏斗的联动跟进记录是CRM系统里最容易被低估的模块。很多团队把它做成一个简单的备注文本框最后客户数据就变成一坨没有结构的流水账。DeskcommCRM里我们把跟进记录设计成“结构化跟进 非结构化备注”混合模式。每条跟进记录保存时业务人员需要选择跟进方式电话/IM/邮件/当面拜访等填写沟通摘要如果有必要勾选一个业务结果比如“已确认需求”“已发送报价”“暂无意向”“推迟到下一周期”等。这些结果字段会作为销售漏斗统计的依据。漏斗统计的数据源不直接查跟进表而是查客户表里的current_stage字段。每次跟进记录保存时业务人员可同步更新客户阶段比如从“初步接触”改成“需求挖掘”系统自动记录一条History。这样做的好处是漏斗统计非常快坏处是阶段变更和跟进记录存在耦合业务方操作起来多了一步。我们权衡后保留了这个设计并在前端做了联动——选择阶段变更时自动生成一条对应类型的跟进记录。3.3 工单管理与桌面通知的打通工单模块本质上是一个轻量级的流程引擎。我们对工单的状态流转做了严格定义待分配、处理中、待客户确认、已关闭、已重新开启。核心操作是分配Assign、处理Process、提交解决Resolve、关闭Close。工单和客户的关系是一对多为了防止一个工单被误关联到错误客户表单里加入了“带出最近3个关联客户”的下拉搜索避免手工输客户名产生的脏数据。这里重点说桌面通知。当时我们想实现“工单被分配、被评论、状态变化时当前负责人的浏览器右上角实时弹通知”最初用HTTP轮询每秒查一次太耗资源后来改成了WebSocket连接加Redis消息订阅。流程是后端更新工单状态后往Redis的某个Channel发布一条消息WebSocket处理器订阅这个Channel收到消息后推送给指定用户的前端连接。用户的在线状态存在Redis里key是userIdvalue是WebSocket session id集合。这个方案的优点是解耦消息发布者和接收者不需要直接知道对方。实际运行中遇到过连接断开重连、多端登录互相踢下线的问题后面在常见问题段落展开说。4. 实操过程我用一个周末搭出可用原型4.1 环境准备与初始化很多人问DeskcommCRM这种系统从零开始搞要多久我的答案是如果只是可演示、可操作的版本一个周末足够。前提是前面数据模型和需求梳理已经清楚代码只是把模型翻译成页面和接口。环境准备阶段我建议直接用Docker Compose把PostgreSQL、Redis、后端、前端一次性跑起来避免每台机器上手工装环境。这里贴一份简化版的后端application.yml关键配置spring: datasource: url: jdbc:postgresql://localhost:5432/deskcomm_crm username: crm_user password: crm_pass jpa: hibernate: ddl-auto: update properties: hibernate: jdbc: batch_size: 50 order_inserts: true redis: host: localhost port: 6379注意ddl-auto开发环境可以用update生产环境一定要改成none并用Flyway做版本管理否则哪天实体类改了一个字段线上表结构直接被自动更新出问题想回滚都难。数据库初始化脚本里优先建这几个表sys_user用户表、crm_customer客户表、crm_lead线索表、crm_follow_up跟进记录表、crm_ticket工单表、sys_notification通知记录表。建立完成后先插入管理员账号再插入两个测试销售账号方便后续验证权限。4.2 核心页面与接口的实现顺序前后端联调最怕的是同时开发所有页面最后到处是接口对不上。我的建议是第一优先级做客户列表和客户详情这是整个系统的心脏第二做跟进记录第三做线索池和工单第四做通知中心和仪表盘。接口设计上RESTful风格加统一返回结构。统一返回结构长这样{ code: 0, message: success, data: {} }code为0表示成功非0表示业务错误。业务错误码不要随便从1开始排建议按模块分段客户模块1000-2000线索模块2000-3000工单模块3000-4000这样排查问题时能快速定位是哪个模块出的错。客户列表接口的筛选条件比较典型分享一个比较实用的参数设计GET /api/customers ?page1 pageSize20 keyword客户名模糊搜索 tags标签筛选多个用逗号分隔 ownerUserId负责人 stageSALES_LEAD销售阶段 dateRangeStart2024-06-01 dateRangeEnd2024-06-30后端在接收参数时不要把keyword塞进like里就完事一定要用参数绑定的方式拼SQL避免SQL注入风险。如果使用Spring Data JPA可以用Specification实现动态查询如果使用MyBatis注意在XML里写动态select时对keyword做转义处理。4.3 数据迁移与历史数据清洗原型跑通之后紧接着就是数据迁移。这是我们项目里最麻烦的一环。旧数据分散在Excel表格和另一个老系统里老系统导出的数据质量很差——同一个客户在不同Excel里可能有三种不同的名称写法比如“北京华信科技有限公司”“华信科技”“北京华信科技”。我们做数据清洗时制定了一套优先级规则统一社会信用代码存在时以此作为唯一标识合并所有关联信息。没有信用代码时使用“归一化客户名称”匹配做法是去除名称中的括号、数字以外的特殊符号再把“有限公司”“股份有限公司”等后缀去掉后做哈希比对。前两条都无法匹配的按名称模糊匹配加人工确认来处理每天清洗一批避免一次性全量清洗导致误合并。清洗完成后把清洗记录单独存放一张migration_log表记录每个客户ID从哪几个源系统来、合并规则是什么、操作人是谁。这样即使后续发现合并错了也能追溯和拆分。5. 常见问题与排查技巧实录5.1 并发冲突与重复客户合并问题系统上线没多久就遇到两个销售同时提交新客户数据库里出现重复客户。原因就是前面说的唯一索引只建立在credit_code上而大多数客户没有填写信用代码。后来代码里做了一个“提交前检查”: 用规范化后的客户名去数据库做精确查询如果存在且负责人不同则弹出提示“可能已有相同客户请确认是否合并”。但并发情况下二次检查还是会有窗口期。最终我们引入了PostgreSQL的“咨询锁”advisory lock在Web应用层面对“客户名哈希值”加锁保证同一时刻只有一个请求能创建同一哈希值的客户。示例代码Transactional public Long createCustomer(CustomerCreateRequest request) { String normalizedName normalizeName(request.getName()); long lockKey Hashing.murmur3_128().hashString(normalizedName, StandardCharsets.UTF_8).asLong(); jdbcTemplate.execute(SELECT pg_advisory_xact_lock(?), lockKey); // 再次检查重复然后插入 return customerRepository.save(buildEntity(request)).getId(); }每次业务操作都加名字哈希的锁性能确实会有一点点损耗但换来的效果是重复客户从源头锐减。上线之后我们新建客户的重复率从原先的8%降到了0.5%左右。5.2 通知不触达、消息丢失怎么排查WebSocket通知是这个项目里用户感知最强的功能也是最容易出问题的一部分。上线第二周就有用户反馈工单明明被分配给我了但什么提醒都没弹。刚开始怀疑是WebSocket连接断开后来排查发现连接是正常的问题是Redis消息订阅的消费者在处理消息时session为null导致推送失败。这里要解释一下原因用户可能在多个标签页都登录了系统每个标签页会创建独立的WebSocket连接。我们在Redis里存的session是userId对应一个连接新连接建立时会把旧的session顶掉旧连接就失效了。但用户实际正在浏览的是旧标签页服务端已经把消息推给了新连接新标签页在后台没弹通知用户自然就看不到了。解决思路是“一个用户对应多个session”Redis里userId对应的value从单个session改成session集合。推送时向所有session发送前端收到消息后不管当前页面是不是激活状态都先在通知中心攒着标题栏闪动点开就是详细内容。这个改动看起来简单但排查过程挺曲折的建议做桌面通知交互的团队提前把“多端在线”这个场景想清楚。另外还有一个很容易被忽略的问题消息推送成功了但浏览器没弹通知。很大原因是浏览器权限设置里通知权限被关掉了。我们在通知中心页面上加了一个引导模块如果检测不到通知权限就提示用户去浏览器设置里打开同时在右上角横幅提醒这才把“消息丢失”的真实比例降下来。5.3 性能慢查询优化与索引设计DeskcommCRM上线两个月后客户数据量来到几十万列表页开始变慢尤其是按标签筛选这种查询第一次加载经常要两秒以上。我们用PostgreSQL的EXPLAIN ANALYZE查看执行计划发现大多数慢查询都卡在customer表的全表扫描和tag_array的过滤上。针对客户列表页的常见查询我们一共加了三类索引组合索引(deleted_at, owner_user_id, updated_at)因为列表页默认查询条件就是“未删除 我的客户 按更新时间排序”。GIN索引tag_array字段支持arraycontains的快速过滤。JSONB的GIN索引extra_attributes字段当用户按扩展字段筛选时走索引而不是全表扫描。建索引对应的SQL大致是这样CREATE INDEX idx_customer_owner_update ON crm_customer (owner_user_id, updated_at DESC) WHERE deleted_at IS NULL; CREATE INDEX idx_customer_tags ON crm_customer USING GIN (tag_array); CREATE INDEX idx_customer_extra_attrs ON crm_customer USING GIN (extra_attributes);加完索引后列表查询从接近两秒降到了两百毫秒以内。这里给个通用建议不要在加索引之前盲目搭建大数据分析组件先分析慢查询日志找到真正频繁执行的SQL再针对性加索引成本最低。PostgreSQL的pg_stat_statements插件可以看到哪些SQL累计执行时间最长这个信息非常关键。此外跟进记录表和工单表也是数据增长的大户。跟进记录的查询基本总是带着customer_id我建了(customer_id, created_at)的组合索引工单表则建了(status, assignee_user_id)组合索引专门支撑“某人待处理工单”的列表查询。建索引不能一步到位要随着数据量和查询模式的变化持续做调整每过几个月就顺手跑一遍慢查询日志看看有没有新的问题SQL这个习惯很值得养成。6. 从复盘角度给后来者的三个提醒第一CRM系统的灵魂是“数据模型 权限模型”不是页面好不好看。很多人一开始就纠结前端框架、组件库我建议反过来——先跟业务方把所有字段、状态流转规则、数据归属规则聊清楚然后画ER图ER图通过了再动手写代码。DeskcommCRM为什么落到最后没有推倒重写就是因为前期数据模型设计花了足够多的时间后期改动的只是页面和接口表结构基本没动过。第二桌面通知这类“锦上添花”的功能一定要提前设计好异常路径。用户关闭通知权限、多标签页登录、浏览器休眠、WebSocket断线重连这四种情况全部要测试通过才能上线。我们第一次上线就是只测了“正常弹窗”这一个路径结果真实环境下各种不弹导致用户对系统产生了不信任后面花了很大精力才挽回体验。第三数据清洗和迁移一定要留审计痕迹。旧系统数据导入后一定要保证每个客户、每张工单都能追溯到来源。尤其当你做客户合并时一旦并错了没有审计日志就无法恢复那可比功能缺陷严重得多。最后再分享一个小技巧。如果你后续打算给CRM加仪表盘、做销售预测建议在立项之初就给关键表加上created_at和updated_at并且统一用数据库时间戳不要用应用服务器时间。这样后续做同期对比、计算响应时长、做趋势分析时时间口径才一致省去大量数据对齐的麻烦。DeskcommCRM做了半年多最大的收获不是代码量而是理解了CRM本质上是一个围绕“信息流转”的系统。客户数据在谁手上、跟进要不要提醒、工单是否超时每一个细节背后都是管理和协作的问题。系统只是把规则固化下来让信息流动得更顺畅。希望这篇复盘能给你一些参考少走一些我们走过的弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询