ARTICLE DETAIL

资讯详情

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

自研CRM实战:从客户表设计到通讯工作台整合

自研CRM实战:从客户表设计到通讯工作台整合 1. 为什么我会去做一个叫 DeskcommCRM 的系统先说背景。上一家公司做的是客服外包业务客服团队常年要切五六个软件接电话用一套看在线聊天用一套查客户订单用一套记录跟进日志又得打开 Excel 或同事自建的共享表格。每天下班前客服主管要花一个小时汇总“今天聊了几个客户、成交了几单、哪些客户得明天跟进”数据来源全靠人工从不同窗口复制粘贴。混乱到什么程度同一个客户早上在聊天工具里问过报价下午打电话来接电话的人根本不知道他是谁又从头问一遍。客户烦客服累主管拿不到准确的数据。后来我们决定不再买现成的 SaaS CRM也不是因为钱而是业务确实特殊——客服团队同时承担销售线索初筛、售前咨询、售后跟进三件事标准 CRM 的“销售管道”模型和“工单”模型都覆盖不全。我和另一个后端同事花了大概四个月从零做了一套内部系统名字就叫 DeskcommCRM。思路很简单把“客户通讯工作台”和“客户关系管理”揉到同一个界面里让客服在一个页面上完成“接待—识别—记录—跟进”的全流程。这篇文章不打算做全面系统讲解重点写我在设计和实现过程中真正被卡住的几个环节以及同样的需求换一种方案会有什么不同的代价。如果你也在做类似“通讯 业务系统”的整合或者正在纠结自研 CRM 的功能边界这篇应该对你有用。2. 不带业务视角的客户表设计三个月后一定返工2.1 一开始我只设计了“客户基础信息”结果被骂了第一个版本我参照网上常见的客户表设计字段就是姓名、手机号、邮箱、公司、备注当时觉得够了。结果上线第三天客服就跑来提需求“客户是通过哪个渠道来的是百度推广、朋友介绍、还是有赞店铺这个得标清楚月底要算各渠道的转化。”我当时直接在客户表加了channel字段值是字符串。又过了一周需求变成“同一个客户既在微信咨询过又在电话里沟通过这两个来源怎么算我们要看最后成交算哪个渠道。”字符串字段根本没法回答这个问题。于是返工。把“客户”和“客户来源”拆成两个概念客户本身是主体一条客服记录属于一次“会话”一次“会话”挂一个渠道。每个客户可能有多条会话记录每一条都有自己的渠道归属。这样才能算清楚“首次来源”“最近来源”“成交来源”三个指标。2.2 客户识别手机号不是唯一标识别偷懒客服领域最头疼的是去重。DeskcommCRM 接入了电话、在线聊天、邮件三个入口同一个客户可能用不同手机号留过资料甚至同一部手机号码在不同渠道里的昵称还不一样。第一次我天真地拿手机号做unique key结果导入历史数据的时候直接崩了同一个人在公司官网留了座机在微信留了手机号两条记录被判定成两个客户。后面做合并功能时才知道真实场景里“哪些记录属于同一个客户”永远不是百分百确定的事只能通过“标识字段 置信度”来合并。我的做法是维护一张customer_identity表主键是identity_id关联字段customer_id、identity_typephone / email / wechat_openid / qq / 企业微信id、identity_value、confidence0~1客户主记录只存customer_id不直接存手机号匹配规则是队列式的新会话进来时按顺序尝试精确匹配手机号 → 精确匹配邮箱 → 匹配微信 unionid → 匹配“手机号后四位 姓名”。匹配到任何一个就归并到同一个customer_id下面。匹配不到就新建。confidence我暂时只用了 0 和 1因为再细分下去算法成本太高对客服场景帮助有限。注意匹配规则的顺序很关键。手机号和邮箱应该始终优先用精确匹配其次是“平台内唯一 ID”比如微信 openid。模糊匹配后四位 姓名只能作为兜底因为它误伤率高同一个手机尾号的人很多。2.3 客户状态字段不能只有一个“状态”一开始我做了status字段潜在客户、意向客户、成交客户、流失客户。客服实际操作时又反馈“昨天刚标记为意向客户今天老板让我查一下这个客户是哪个销售负责的还有他上次跟进是什么时候”这些信息如果塞在status一个字段里什么都查不了。最终改成了三个独立维度stage客户所处销售阶段新线索、联系中、需求明确、报价中、成交、流失owner_id当前负责人可能同时存在“首次接待人”和“当前跟进人”last_follow_up_at最后跟进时间用独立字段存而不是靠翻聊天记录看这个设计看起来很朴素但真正用起来才发现价值每周一早上跑一次last_follow_up_at 7天的列表就是“需要回访的客户清单”完全不需要额外建报表。3. 把通讯入口收进工作台是 DeskcommCRM 最重要的一步3.1 不是做“呼叫中心”而是做“通讯动作聚合”最初有同事建议直接接入云呼叫中心 SDK自动录音、自动弹屏。我评估了一下如果要做到完善的电话托管、排队、IVR 语音导航工作量至少多出一个月而且呼叫中心必须由具备资质的话务通道转发成本很高对内部系统来说名大于实。DeskcommCRM 的做法是“软电话 点击外呼”客服电脑插话机或使用 USB 话务耳机通过 WebRTC 调起浏览器电话系统内部存的是call_log表记录通话时间、时长、对方号码、录音文件外链、通话结果未接通/接通/意向明确/已加微信客服在系统里点“拨打”按钮系统调用话务网关 API 发起外呼通话结束后强制填写“通话小结”才能关弹窗这么做避免了“必须做全套呼叫中心”的陷阱同时保留了最关键的一环通话数据自动归档到客户档案。哪怕只是接通了五秒通话记录也在不会出现“打电话给客户但系统里没留下任何痕迹”的情况。3.2 在线聊天记录的统一收口在线咨询入口也类似。我们没有自研 IM而是直接接入了企业微信客服和企业微信会话存档。客服在 DeskcommCRM 里看到的是一个统一收件箱里面同时汇总了企业微信私聊、客户群、公众号消息点击任意一条消息右侧侧边栏同步显示客户资料和订单记录。这一块的难点不在接入在“消息与客户档案的关联”。企业微信消息会提供external_userid外部联系人 ID但同一个客户可能同时加了客服的企业微信、进过客户群、又在公众号留过言。我用前文提到的customer_identity来统一关联凡是外部联系人 ID 相同的情况直接绑定凡是没有绑定过的就展示为“未知访客”由客服手动认领。3.3 所谓“桌面化”就是把高频操作放到一个页面里“Deskcomm”这个名字后半截指的就是这个逻辑。传统 CRM 列表在左详情在右查看聊天记录要再点一个新页面。DeskcommCRM 的工作台是三栏布局左栏会话/工单列表按紧急度排序中栏当前会话的聊天内容右栏客户档案、历史来访记录、待办任务、订单信息这个布局一点都不新颖但真的解决了一个关键问题客服不用来回切换窗口整个接待过程中的所有信息都在一屏之内鼠标移动次数少了单次会话时长明显下降。上线两周后我统计过客服处理一个咨询的平均耗时从 4 分 20 秒降到了 2 分 50 秒一屏式信息展示的贡献很大。4. 工单系统和 CRM 的边界什么时候该“分出去”什么时候该“并进来”4.1 工单不是大而全的而是“异常处理”的载体CRM 管的是普通商机推进流程——联系客户、发方案、报价、签单。但实际运营中总有“异常流程”客户投诉、售后维修、技术问题转交、跨部门协作。这些不该在“客户进度”里硬塞否则看数据的人分不清“这个客户走到哪一步了”和“这个客户有什么待处理问题”。DeskcommCRM 的工单模块只做四件事创建任何客服可以为一个客户创建工单选择工单类型投诉/售后/技术/其他流转主管可以指派给某个处理人或某个小组时效记录每张工单的创建时间、响应时间、解决时间超时自动标红关联工单必须关联一个customer_id也可以关联一个订单 ID不做 SLA 计算不做多级审批流不做工单模板扩展。不是说这些功能不好而是我们团队只有两个人做太多配置化功能会陷入“功能森林”最后反而没人用。一个系统能坚持用下去核心是“知道不该做什么”。4.2 一个典型工单流转示例举个例子客户在微信里说“我上个月买的机器坏了想报修”。客服在聊天里看到后点击客户名旁边的“创建工单”。类型售后优先级中描述客户反馈机器故障附上客户描述原文指派直接指定给小张售后工程师小张在 DeskcommCRM 里看到待处理工单列表点开工单左侧能看到这个客户的历史全部通讯记录和购买记录。他在工单里回复“已联系客户确认是电源板故障安排补发电源板”系统自动给客户发一条微信通知。工单状态从“处理中”改为“待客户确认”。第三天客户反馈换新后正常小张把工单关闭。整个过程中数据天然沉淀在客户档案里不需要任何额外录入。4.3 工单数据不要和会话数据混在一张表我踩过一个坑最初想省事把所有“客户联系动作”统一存到interaction表然后靠type字段区分是聊天、电话还是工单。结果type字段越来越多查询越来越慢每次统计都要写一堆CASE WHEN。后来老老实实拆表call_log通话记录message_log在线聊天记录ticket工单三张表通过customer_id关联统计报表时用 JOIN 或子查询。这样做的代价是代码稍微啰嗦一点但每张表的字段都简洁清晰维护成本低很多。5. 跟进记录的自动化和 UI 细节比业务逻辑更影响日活5.1 强制“跟进小结”和“下次跟进时间”很多系统把跟进记录当成可选项客服忙起来就不填最后报表是空的。DeskcommCRM 做了一个不近人情的设定每次通话结束或聊天会话关闭时弹窗必须填写两个字段——跟进小结、下次跟进时间否则不能关窗口。这个设计来自我们客服主管的一句话“你们系统能不能帮我盯着每个人别让客户在他们手里断掉”强制弹窗保证了“每个客户被处理过之后一定留下痕迹”。一开始有客服抱怨麻烦坚持了两周以后大家习惯了因为好处很明显——下次再碰到这个客户翻记录就知道上次聊到哪不用重新问一遍。5.2 跟进任务自动生成“下次跟进时间”填完之后系统自动创建一条待办任务放到负责人的“今日待办”列表里。到期当天早上 9 点推送提醒超期未完成则任务标红主管后台能看到超期任务数。不用额外做复杂的 CRM 工作流引擎一张follow_up_task表就能搞定task_idcustomer_idowner_iddue_atstatuspending / done / skippedrelated_typecall / message / ticketrelated_id每天定时任务把due_at 当天 23:59:59 且 status pending的任务推给负责人就这么简单。做复杂工作流引擎的团队往往真正用起来的还是这种“看一眼就知道该干嘛”的待办。5.3 客户详情页的“时间线”必须按倒序这个细节很小但影响很大。客户详情页我一开始是按时间正序展示所有互动记录通信、工单、跟进、订单变更。客服真实操作时发现正序找“上次聊了什么”很难受因为最近的信息沉在最底部。改成倒序之后客服打开客户详情页的第一眼看到的就是最近发生的事情。就这么一个小改动客服反馈“明显好用了”。类似这种交互细节在系统设计时就该优先考虑一线使用者的真实动作而不是按数据生成的逻辑来排列。6. 数据报表先搞定“日活看得懂”再谈“老板看得爽”6.1 一屏日报每人每天多少通电话、多少条消息、处理了多少工单DeskcommCRM 的报表模块没有从一开始就做大屏可视化而是先做了一个最朴素的“日报表”页面默认展示今日每人外呼次数、接通次数、通话总时长今日每人接待消息数、响应平均时长今日工单新增数、关闭数、超时数今日新增客户数、跟进到期数这个页面用列表形式展示不用图表。因为客服主管实际需要的就这些数字不需要炫酷的折线图。上线后主管每天早上打开 DeskcommCRM 看一眼就知道昨天团队整体情况省去了原来手工汇总的一小时。6.2 转化数据怎么算界定好“一次客户旅程”要想算“从线索到成交的转化率”先得定义“一次客户旅程”的起始和结束。这是所有 CRM 里最容易扯皮的地方——是首次咨询算起点还是建了客户档案算起点成交是首付款日算还是合同签订日算我的选择是起点 首次message_log或call_log记录的当天终点 订单表里第一个“已支付”状态的订单创建时间。这个定义可能不适合所有业务但关键不是“正确”而是“一致”——所有人都按同一个口径看数才能对比分析。口径不统一才是报表项目失败的最大原因。6.3 不要做实时报表除非你能扛住数据量我一开始计划用 WebSocket 推送实时看板数据每隔几秒刷新一次。后来发现客服团队根本不需要“实时”他们需要的是“第二天早上能看到昨天的准确数据”。所以报表模块只做了离线统计定时任务在每天凌晨跑一次把前一天的汇总数据落库页面直接查统计结果表查询速度飞快也不需要单独的报表数据库。当你团队规模不到几十人时实时大规模计算完全没必要。CPU 不用白不用但人力是要花钱的优先做对使用者真正有价值的功能。7. 上线之后的三个“真实事故”和对应修补7.1 事故一导入历史数据时把两个同名客户合并错了导入数据来自 Excel同一客户在表格里出现两次一次手机号写了 138一次写了 139可能是旧号和新号。我的匹配机制在“手机号精确匹配”阶段没匹配上然后在“姓名 后四位”兜底时误判成同一个人结果把两人的订单合并了。修补方案所有自动合并操作都先进“待确认合并”列表由主管人工确认后才真正合并。这个列表每天要清一次但误合并的问题再没发生过。7.2 事故二企业微信消息侧的“同一个人”识别错乱客户在企业微信里同时加了客服 A 和客服 B两个人各自在 DeskcommCRM 里看到了“不同客户”因为external_userid是以每个客服为维度的而不是企业统一维度。修补方案调用企业微信“客户联系”接口获取“外部联系人详情”里面有一个unionid字段只要客户在微信生态内统一绑定过手机号或公众号就能用来做全局标识。我把unionid也加入customer_identityexternal_userid只作为会话关联的辅助键。7.3 事故三跟进任务超时提醒太吵最后没人看第一版我设了每两小时推送一次催办通知结果客服直接把通知权限关了系统里“待办已读率”降到 20%。后来改成每天 9:00、11:30、16:00 三个节点推送并且只推和自己相关的任务已读率回升到 85% 以上。“提醒”这个功能最关键的其实是克制。过度打扰会让用户习惯性忽略反而起不到提醒效果。8. 一些你可能需要的具体实现参考8.1 表结构简表客户与互动域-- 客户主表 CREATE TABLE customer ( customer_id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100), gender TINYINT, company VARCHAR(255), remark TEXT, stage VARCHAR(50), owner_id BIGINT, last_follow_up_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 身份标识表用于多渠道识别 CREATE TABLE customer_identity ( identity_id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, identity_type VARCHAR(30), -- phone / email / wechat_openid / unionid identity_value VARCHAR(255), confidence DECIMAL(2,1) DEFAULT 1.0, UNIQUE KEY uniq_type_value (identity_type, identity_value) ); -- 通话记录 CREATE TABLE call_log ( call_id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT, owner_id BIGINT, phone_number VARCHAR(30), direction VARCHAR(10), -- inbound / outbound started_at DATETIME, ended_at DATETIME, duration_seconds INT, call_result VARCHAR(50), -- connected / no_answer / busy / invalid summary TEXT, recording_url VARCHAR(500) ); -- 消息记录 CREATE TABLE message_log ( msg_id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT, owner_id BIGINT, channel VARCHAR(30), -- wecom / public_cloud / web_chat sender_type VARCHAR(10), -- customer / agent content TEXT, sent_at DATETIME, KEY idx_customer_time (customer_id, sent_at) ); -- 工单 CREATE TABLE ticket ( ticket_id BIGINT PRIMARY KEY AUTO_INCREMENT, ticket_no VARCHAR(50), customer_id BIGINT, order_id BIGINT NULL, ticket_type VARCHAR(50), -- complaint / after_sale / technical priority VARCHAR(10), -- low / medium / high status VARCHAR(30), -- open / processing / waiting_customer / closed assignee_id BIGINT, creator_id BIGINT, description TEXT, created_at DATETIME, updated_at DATETIME, closed_at DATETIME );8.2 获取“待跟进且超期”的任务列表SELECT t.task_id, t.customer_id, c.name AS customer_name, t.due_at, t.status FROM follow_up_task t LEFT JOIN customer c ON c.customer_id t.customer_id WHERE t.owner_id ? AND t.status pending AND t.due_at DATE_ADD(NOW(), INTERVAL 1 DAY) ORDER BY t.due_at ASC;8.3 一个简单的“下一次提醒”判断既然选择了每天定时推送就可以在每天 9 点、11 点 30 分、16 点三个时间点跑一个批处理脚本把符合条件的任务汇总推给对应负责人scheduled_times [09:00, 11:30, 16:00] def remind_job(): pending_tasks db.query( SELECT task_id, owner_id, due_at FROM follow_up_task \ WHERE status pending AND due_at NOW() ) by_owner {} for task in pending_tasks: by_owner.setdefault(task.owner_id, []).append(task) for owner_id, tasks in by_owner.items(): send_notification(owner_id, tasks)这个脚本用简单的 cron 调度即可不需要引入消息队列或任务编排框架维护成本低出了问题定位也容易。9. 部署与运维时要注意的几个实际问题9.1 内网部署还是云服务器如果客服团队少、数据要求高私密可以直接内网部署。DeskcommCRM 后端用的 Java Spring Boot前端用的 Vue 3打包成jarnginx静态资源即可。内网部署注意服务器时区要统一否则“今日跟进任务”会出现晚上 23:59 和凌晨 00:00 的边界判断问题。9.2 录音文件的存储路径通话录音如果直接存数据库数据库很快会膨胀。我把录音文件放到对象存储或本机磁盘目录数据库中只存文件链接。对于中小团队/data/recordings/2025/06/xx.wav这种目录结构就够了。注意定时清理过期录音根据公司保存期限要求设定保留天数否则磁盘很快会被占满。9.3 备份策略客户数据是核心资产但不需要多复杂的备份方案。我用的mysqldump每天凌晨 3 点全量备份一次保留最近 7 天每周日凌晨做一次离线上传保留一份到其他机器。这比高可用集群、读写分离方案更实用毕竟系统挂了可以重建数据丢了才真正要命。10. 最终项目复盘哪些决定现在回想仍然是对的哪些该改如果现在让我重做一遍 DeskcommCRM有几件事我会坚持有几件事我会调整坚持的部分通讯工作台和 CRM 一体化的定位这是这个系统存在的意义强制跟进记录和跟进任务机制它保证了“数据不沉没”把所有客户互动按时间线倒序展示简单但特别有效报表先做朴素列表再做可视化业务方真正关心的是数字不是图形调整的部分一开始就应该引入customer_identity表而不是等合并事故发生了再补企业微信 unionid 识别应该在需求分析阶段就调研清楚别等上线后才发现工单模块可以再轻一点把“工单状态流转”做成纯字段更新不搞状态机更符合小团队实际提醒推送的频次从一开始就该保守提醒功能的“疲惫感”是真实存在的另外有一些想法我想对正在犹豫“要不要自研 CRM”的朋友说。如果你所在公司的业务流程完全通用——Salesforce 那种标准销售管道就能覆盖——那么买现成的确实比自研便宜。如果你的业务流程有大量“跟客户沟通的过程管理”需求尤其是客服团队和客户多次交互、多渠道合并、通话记录与客户档案自动关联那么市面上很多通用 CRM 反而不合适自研一个轻量系统是值得的。DeskcommCRM 的核心不是用了什么高深技术而是精准贴合了“客服需要在一个界面里完成接待 记录 跟进”这个场景。这个场景在很多行业都有但很少有一个标准产品能配得那么好。做好一个内部系统有时候比的不是技术难度而是对业务现场的理解深度。你是把系统做出来让人用还是做了个系统等人来填数据用两周就能见分晓。每次上线前我都会问自己一句“这个功能上线后客服真的愿意每天打开它吗”如果答案犹豫那就别上了。
返回列表