DeskcommCRM这个项目,名字听起来有点长,其实就是Desk Communication CRM,翻译过来是“桌面沟通型客户关系管理系统”。说白了,这就是我们销售和客服团队自己用的那套客户沟通管理工具。做这个系统的初衷特别朴素:我们每天跟客户的沟通记录,散落在微信、企业微信、电话通话记录和一堆Excel表格里,想查一个客户的历史沟通情况,得翻好几个地方,甚至经常出现销售A谈过的客户,销售B不知道来龙去脉,又跑去重复沟通惹恼客户的情况。
这个项目是我带团队从零到一搭起来的,目标很明确:把“客户档案”和“沟通记录”这两件事收拢到一个系统里,让每次跟进都有迹可循,让新接手的人能快速读档。如果你正在考虑给自己团队做一套类似的内部CRM,或者想知道一个轻量级客户管理系统背后有哪些设计和坑,这篇内容应该能给你一些可以直接用的经验。
1. 项目背景与需求拆解
在做DeskcommCRM之前,我先花了大概两周时间做需求调研。很多团队一开始做CRM容易犯一个错误——一上来就想把销售管理、客户分类、商机漏斗、合同回款全塞进去,结果做出来一个四不像,谁都不爱用。我们当时定的原则很简单:只做三个核心场景,其余一概不碰。
1.1 核心业务痛点:沟通记录散落各处
我们团队当时的业务形态是典型的B2B客户服务加二次销售,销售和客服人员每天要接触大量客户。最痛苦的事情是:
- 客户来电话咨询,接待的人换了,新同事对客户之前的情况一无所知。
- 重要客户的生日、合同到期日、回访提醒全靠个人手机备忘录,人一走,信息就带走了。
- 管理层想统计一下“今天有多少客户需要跟进”,根本统计不出来。
这些痛点归纳起来就一句话:客户信息在个人手里,没有沉淀到组织层面。我们做DeskcommCRM,本质上就是把本应该属于公司的客户资产重新“收归公有”。
1.2 功能边界划定:MVP阶段的三个核心模块
项目第一版只做了三个核心模块:客户档案管理、跟进记录管理、待办提醒。每个模块再往下拆就很清晰了:
- 客户档案管理:客户的姓名、公司、联系方式、来源渠道、所属销售、客户状态、备注。
- 跟进记录管理:按时间线记录每一次和客户的沟通内容,支持文字和附件。
- 待办提醒:基于跟进记录自动生成下一步任务,包括一般回访时间、合同到期提醒。
其他什么数据分析、自动标签、营销活动这些,全部排在第二期。做内部系统最怕的就是需求堆砌,第一版一定要小,小到团队能用起来,才会有后续迭代的可能。
1.3 需求调研的方法:和真正的使用者聊
我走访了销售、客服、售后三个角色的实际使用者,发现不同角色对CRM的诉求差别很大。销售关心的是“客户资料好不好找”“跟进记录省不省事”;客服关心“记录能不能快速填充”“能不能看到客户是否有过投诉”;管理层关心“能不能出报表”。
最后我们把这三个角色的共性需求提取出来,形成了核心功能列表。这一步特别值得做,因为你会发现,用户嘴上说的需求和实际工作流里的需求,往往是两回事。只有站在他们座位的角度去看,才能设计出真正有人用的功能。
2. 技术方案选型与整体设计思路
技术选型这件事,没有绝对的好坏,只有合不合适。因为我们团队本身前后端都有,所以选型上相对灵活。这里我把当时的思考和取舍过程写出来,给大家一个参考。
2.1 前端框架:Vue 3 + Element Plus
前端用了Vue 3配合Element Plus组件库。选Vue而没选React,不是因为Vue比React好,而是因为团队成员的熟悉度更高,同时Element Plus在后台管理系统这块非常成熟,表格、表单、弹窗这些常用组件开箱即用,能极大提升开发效率。
页面结构上采用经典的“左侧菜单栏加右侧内容区”的布局,菜单包括“客户列表”“跟进记录”“待办任务”“数据看板”“系统设置”。这套东西没什么创新,但胜在稳定、学习成本低,新员工上手也快。
2.2 后端框架:Spring Boot + MyBatis-Plus
后端用了Java的Spring Boot,搭配MyBatis-Plus做数据访问。Java在这类内部管理系统里的优势不用多说,稳定、类型安全、生态成熟。Spring Boot让我能用最少的配置搭起来一个可用服务,而MyBatis-Plus极大简化了单表CRUD操作,很多通用方法直接继承就行。
接口设计上,统一返回RESTful风格的JSON数据:成功返回{ "code": 0, "data": ... },失败返回{ "code": 非0, "message": 错误信息 }。这套约定看起来简单,但团队协作时避免了大量沟通成本。我还统一加了全局异常处理,业务异常和服务异常都能返回结构化信息,而不是一坨堆栈。
2.3 数据库选型:MySQL + Redis
数据存储用了MySQL,这几乎是企业应用的标配了。表结构设计上,我比较克制,核心表就五张:客户表、联系人表、跟进记录表、待办任务表、用户表。再多就容易乱了。
Redis用在了两个地方:一是登录会话,存token;二是热门客户的查询缓存。一开始我没打算上Redis,是因为后来发现客户列表的查询量越来越大,很多查询是重复的同一个列表,加一层缓存效果立竿见影。
2.4 为什么不用现成的第三方CRM
调研阶段也考虑过直接用市面上现成的CRM产品,但最后放弃了,原因有三:
- 价格不透明,按账号收费,团队人一多成本就上来了。
- 定制需求做不了的场景多,比如我们内部某些字段和业务流程是独有的。
- 客户数据在自己手里才最安心,不想被绑定在某个平台的体系里。
自己做并不是因为第三方不好,而是因为内部系统最核心的价值是可控和定制,这个逻辑要搞清楚。
3. 数据库设计:五张核心表是怎么构建起来的
数据库设计是整个DeskcommCRM的“地基”。这个环节如果偷懒,后面所有功能都会别扭。我在这里花的时间最多,也最建议后来者在这个阶段慢下来。
3.1 客户表:怎么存才能不重复
客户表是系统的核心,我设计了这些关键字段:id、name、company、phone、source、owner_id(归属销售)、status、created_at、updated_at、deleted_at。
这里想强调一个细节:客户表必须做软删除,用一个deleted_at字段标记删除时间,而不是物理删除记录。做内部系统时,经常遇到员工误删客户资料的情况,软删除保证数据可以恢复,同时统计报表不会被破坏。
还有一个很关键的逻辑:客户去重。我们的去重规则是“手机号完全匹配”,在phone字段上建立了唯一索引。真实业务里这个规则可能会漏掉一些客户(比如一个人换了新手机号),但它能挡住90%以上的重复输入,效果立竿见影。
3.2 跟进记录表:不能直接修改的设计
跟进记录表是我个人认为整个系统设计最值得讲的部分。表中核心字段有:id、customer_id、user_id、content、next_action_time、next_action_type、attachments、created_at。
关于这个表,我定了一条铁律:记录只能新增,不允许修改、不允许删除。所有操作只有逻辑删除的选项,由管理员在特定场景下执行。为什么?因为客户的沟通记录是一种“日志型数据”,如果允许任何人随意编辑,很容易出现“翻旧账”的问题——比如某人把之前的沟通内容改掉了,将来说不清是谁的责任。
实际操作中,我给跟进记录接口只暴露了POST(新增)和GET(查询),在代码里根本没写更新和物理删除的方法。这是从接口层就卡死,而不是靠前端隐藏按钮。
3.3 待办任务表:沟通记录的后续延伸
待办任务表设计为独立于跟进记录表,但跟跟进记录是强关联的。它包含id、customer_id、task_type(回访/续费/合同等)、due_time、assignee_id(处理人)、status、related_record_id(关联的跟进记录ID)。
这样设计的好处是,销售每次新建跟进记录时,可以顺手勾选一个“下一步计划”,系统自动生成一条待办任务,并出现在对应人的待办列表中。这个方法减少了重复输入成本,把计划的生成自然地嵌入到日常操作中。
3.4 索引设计:哪些字段需要建索引
数据库表建好以后,索引是直接影响查询性能的。我当时给三张核心表分别建了这些索引:
- 客户表:
(phone)唯一索引、(owner_id)普通索引、(status)普通索引。 - 跟进记录表:
(customer_id)普通索引、(user_id)普通索引。 - 待办任务表:
(assignee_id, status)联合索引、(due_time)普通索引。
索引这东西,宁缺毋滥,因为索引本身也要存储和维护,写频繁的表索引太多反而拖慢写入速度。我们这几张表读取多写入少,索引多建几个问题不大。
4. 核心功能模块的实现细节
接下来是实操环节。这一节我会按功能模块来讲实现细节,重点说清楚“怎么做的”和“为什么这么做”。
4.1 客户列表:分页、搜索和列表缓存
客户列表是所有操作的门面,性能直接决定用户体验。我用了PageHelper分页插件,前端每次请求传pageNum和pageSize,后端返回当页数据和总条数。
搜索这块,因为业务里最常见的场景就是“找个手机号对应的客户”,所以搜索条件重点支持手机号、姓名、公司名称的模糊匹配。但模糊匹配的SQL如果直接LIKE '%关键词%',是走不了索引的,所以我在实现时做了个额外优化:
- 手机号搜索:如果关键词长度大于等于7位且全是数字,按手机号前缀匹配,走索引。
- 名字/公司搜索:只允许包含名称开头的匹配,也走索引。
- 都不满足时,才走全表模糊查询,并加上
LIMIT 50的限制,避免一次查太多。
列表缓存我在Redis里给“常用销售视图”做了60秒的缓存。第一次请求时从MySQL查出来,然后把数据存入Redis缓存,60秒内的后续请求直接读缓存。这个功能上线后,整体列表打开速度确实快了很多,尤其是销售员工每天反复查看自己客户列表的场景,体感改善明显。
4.2 跟进记录:新增记录的完整流程
新增跟进记录是整个系统里最重要的操作。我在实现后端接口时,处理了一个关键问题:事务一致性。
一个完整的“新增跟进记录”流程包括:
- 校验客户ID是否存在,是否有权限编辑。
- 写入跟进记录表。
- 如果勾选了“生成下一步任务”,则同步写入待办任务表。
- 更新客户表的
updated_at字段,方便列表按最近跟进时间排序。 - 如果指定了下次回访时间,把该时间同步到客户的
next_follow_time字段。
这五步必须放在同一个事务里,任何一个环节失败都要全部回滚。如果拆成多个请求各自单独执行,就会出现“记录写进去了但任务没生成”这种脏数据,很难排查。
核心伪代码大致是这样的:
@Transactional public Long addFollowRecord(FollowRecordCreateDTO dto) { // 1. 校验客户权限 Customer customer = customerMapper.selectById(dto.getCustomerId()); if (customer == null) { throw new BusinessException("客户不存在"); } // 2. 写入跟进记录 FollowRecord record = new FollowRecord(); record.setCustomerId(dto.getCustomerId()); record.setUserId(currentUserId()); record.setContent(dto.getContent()); followRecordMapper.insert(record); // 3. 生成待办任务 if (dto.getNeedTask() != null && dto.getNeedTask()) { TodoTask task = new TodoTask(); task.setCustomerId(dto.getCustomerId()); task.setDueTime(dto.getNextActionTime()); task.setAssigneeId(dto.getAssigneeId()); task.setStatus(0); // 未完成 task.setRelatedRecordId(record.getId()); todoTaskMapper.insert(task); } // 4. 更新客户表 Customer update = new Customer(); update.setId(dto.getCustomerId()); update.setNextFollowTime(dto.getNextActionTime()); customerMapper.updateById(update); return record.getId(); }4.3 待办提醒:基于时间的自动推送
待办任务做出来后,最大的挑战是如何“让该看到的人看到”。我当时的方案是:不做即时推送,改成“进入页面的轮询查询”。
也就是说,用户打开待办页面时,后端自动查询当前用户所有未完成的任务,并且按照due_time排序,把过期的放在最前面。过期任务用红色标识,当天任务用黄色标识,未来任务用正常色。
后面还做了一层“每日待办通知”,通过企业微信机器人每天早上9点把当天有到期任务的销售名单推送到群里。这个实现不复杂,一个定时任务就能搞定:
@Scheduled(cron = "0 0 9 * * ?") public void sendDailyReminder() { List<TodoTask> todayTasks = todoTaskMapper.selectTodayTasks(); // 按 assigneeId 分组,组装每个销售的任务清单 // 调用企业微信机器人 Webhook 发送消息 }不过这里要注意一点:定时任务的执行时间要避开业务高峰。我们当时定时任务都安排在凌晨或者早上上班前,一定不要在中午业务高峰期跑大批量任务,否则会拖慢正常接口。
4.4 权限控制:前后端配合的越权防护
权限控制是DeskcommCRM里很容易被忽略但绝对不能省的部分。我们的权限模型分三层:
- 管理员:看到所有客户的资料,管理整个系统。
- 主管:看到自己所在小组的客户。
- 普通销售:只能看到自己名下客户和主动共享给团队的客户。
后端用一个拦截器解决大部分权限判断——从请求头里取token,解析出当前用户ID和角色,再在具体接口里做数据范围的校验。关键方法如下:
public boolean hasAccessToCustomer(Long customerId) { // 管理员直接放行 if (isAdmin()) return true; // 普通用户校验 customer.owner_id 是否等于当前用户ID Customer customer = customerMapper.selectById(customerId); if (customer == null) return false; return customer.getOwnerId().equals(currentUserId()) || isTeamShared(customerId); }前端菜单和按钮也做了对应的权限控制。但前端控制的目的只是优化体验,真正安全的防线一定在服务端。任何时候都别相信前端传来的参数,后端必须验证客户ID和当前用户的关系。
4.5 看板统计:用分组查询算出团队绩效
数据看板在第二版加上去的,主要给管理层看三个指标:每个销售的客户总数、今日新增跟进数、待办完成率。实现其实很简单,就是几条SQL的聚合查询:
SELECT owner_id, COUNT(*) FROM customer WHERE deleted_at IS NULL GROUP BY owner_id;这类统计查询不需要实时,所以我做了一层异步缓存,每天凌晨把前一天的统计数据计算好,放到一张独立的统计表里,管理界面的看板直接读统计表,不查业务表。这样即便业务量涨上来,统计查询也不会拖垮主流程。
5. 系统实现过程中的常见问题与排查技巧
项目开发过程中踩了不少坑,有些问题看着不大,但解决起来很费时间。这里挑几个典型的写出来,希望大家能少走弯路。
5.1 客户数据重复:索引没生效的原因
第一版上线后两个星期,我们就发现了客户重复问题。排查后发现问题出在一个很隐蔽的地方:phone字段在MySQL里定义成了VARCHAR(20),但有个接口在写入前做了字符串的trim处理,而另一个导入接口没有做,导致“138 1234 5678”和“13812345678”被当成了两个不同客户。
解决思路是:在服务端入口统一处理手机号格式化,所有写入都走同一个normalizePhone方法,去掉空格、横线等分隔符。同时在手机号唯一索引的基础上,把客户录入接口改成先查询再插入,查不到才新增,从接口层防止重复。
5.2 查询慢:模糊搜索拖垮了列表页
客户列表页在数据量涨到三万条之后变得很慢,单次查询要两三秒。排查发现罪魁祸首是一个LIKE '%关键词%'的模糊查询,如果是全表模糊搜索,数据量一大就是灾难。
我们的调整方案是:
- 查询条件改成多字段分别匹配,手机号前缀走索引。
- 把模糊搜索改成“必须输入至少两个字符才触发”。
- 统一加上1秒的Redis缓存。
调整后列表响应时间降到了200毫秒以内。给一个建议:凡是涉及比较耗时的查询,都先想清楚这个查询频率高不高、是不是可以加缓存,这比事后调优舒服得多。
5.3 并发编辑导致的数据覆盖:字段级更新 vs 整行更新
跟进记录的“下一步任务”如果生成两次,会出现待办任务重复的问题。原因是两个请求同时进来,都看到了客户当前没有未完成任务,就各自插入了一条。
解法是给待办任务表的customer_id加上“未完成唯一约束”的功能。MySQL里没法直接在部分行上建唯一索引,我最终是额外加了一个字段active_flag,当任务未完成时设置为固定值1,完成之后变为0。然后建联合唯一索引(customer_id, active_flag),这样未完成的任务在同一客户下只能存在一条,已完成的可以有多条。这个方案用一个库存的小技巧解决了并发问题。
5.4 配置文件泄漏:本地环境的几个坑
开发过程中每个人的本地环境配置不一样,出现过几次“本地跑得好好的,部署到服务器就报错”的问题。后来我们统一用application-dev.yml、application-prod.yml区分环境,把数据库连接、Redis连接、机器人Webhook地址这些配置全部外置,并在代码里写清楚必须从配置中心读取,不再允许硬编码。
5.5 排查工具:一把好用的瑞士军刀
顺便推荐几个排查问题的小工具:
- Arthas:线上JVM诊断工具,能直接看接口耗时、方法调用参数、异常堆栈,对排查线上问题非常有帮助。
- Flyway:数据库自动版本控制,每次表结构变更都写一个迁移脚本,团队协作不会把库搞乱。
- Postman/APIFox:接口调试和文档管理,前后端协作效率能提升一大截。
6. 部署上线与推广使用
开发完成后,上线部署这块也值得一提。我们自己有一台内网服务器,部署方案用的是:Nginx做反向代理,前端打包成静态文件丢在Nginx里面,后端用java -jar跑一个Spring Boot包,数据库用MySQL 8.0。
为了让团队真正愿意用,我在上线时做了三件事:
- 数据导入:把原有Excel里的客户数据批量导入到系统,让团队打开系统就能看到熟悉的客户,而不是空空如也。
- 演示培训:组织两场实操演示,专门讲“怎么用、对公司有什么好处”,让每个人都能当场试一遍。
- 树立标杆:让部门里一位比较有影响力的销售先用起来,做出几个成功案例,其他人看到效果自然会跟上。
上线后的第一周我还特意每天看一眼后台日志,哪个页面访问最多、哪个接口报错最多,第二天就修,尽量保证第一印象是好的。内部系统最怕的是一开始体验差被大家抛弃,一旦被抛弃就很难再拉回来了。
7. 项目复盘:做内部系统的一些体会
DeskcommCRM到现在稳定运行了大半年,团队的使用率维持在九成以上。回看这段经历,我个人最大的感受是:做内部系统,赢在“懂业务”,而不是“炫技术”。
很多技术方案看起来高大上,但如果脱离了团队实际工作场景,做出来的东西只会成为负担。我们全程克制地控制功能范围,先把核心沟通链条打通,持续根据反馈迭代,才让这套系统真正扎根下来。
另一个经验是:要特别重视非功能性需求。响应速度、权限安全、数据备份,这些看起来不起眼,但对用户的日常使用体验影响极大。一次白屏、一次数据丢失,可能就会让用户对系统的信任度大幅下降。
最后想给正在做类似项目的人一个建议:内部系统的“成功标准”不是代码写得多优雅,而是团队每天真的在用它,并且愿意把里面的数据当作唯一可信的信息源。能做到这一点,这个项目就已经成功了。