轻量级CRM系统开发实战:DeskcommCRM从0到1

发布时间:2026/9/16 20:22:22
轻量级CRM系统开发实战:DeskcommCRM从0到1 做客户管理这件事很多人一开始是拿Excel凑合的客户少的时候没问题等客户超过一两百个你会发现漏跟进、记错人、翻聊天记录翻到眼瞎数据散在微信、邮件、通话记录里谁跟了什么单完全靠脑子记。DeskcommCRM 这个项目就是我在这个背景下动手做的一套轻量级客户关系管理系统核心思路是“围绕沟通管客户”把每一次和客户的互动串成一条时间线再配合跟进提醒、销售漏斗和基础权限隔离让一个小团队不需要昂贵的CRM平台也能把客户盘清楚。这篇文章我会把整个项目的需求拆解、数据模型、核心功能实现、落地过程以及上线后踩过的坑完整写出来适合正在用Excel管客户、准备自己搭一套内部CRM的开发者和业务负责人参考。1. 项目定位与需求拆解1.1 这个CRM到底解决什么问题很多现成的CRM系统功能很全但小团队用起来其实很痛苦。一是价格不便宜按用户数收费的SaaS一年下来够请一个人了二是流程太死板标准产品逼着你去适配它的字段、审批流和权限模型三是数据不在自己手里客户信息放在第三方的服务器上心里总觉得不踏实。DeskcommCRM从一开始就没打算做成大而全的体系它的目标非常明确把客户信息、沟通记录、跟进计划和成交状态四件事管好。我经历过一个典型场景销售在微信里和客户聊得挺好转头开会一忙三天没联系客户被同行截胡售后同事处理完一个问题没有记录下次客户再问类似问题整个团队又从头开始查。这些问题的根源不是“人不够勤快”而是“信息没有沉淀”。DeskcommCRM解决的就是沉淀问题每一次沟通都留下痕迹每一个客户都有唯一的档案每一次跟进都有明确的下一步动作。这样不管谁来接手都能在十分钟内搞清楚这个客户的全貌。1.2 目标用户与适用场景这个系统我设计之初主要面向三类使用者销售、售后客服、团队管理员。销售关心的是哪些客户该跟进、我现在有哪些商机售后客服关心的是历史沟通记录和问题处理进度管理员关心的是每个人的工作量、客户池的健康度以及数据权限。适用场景我归纳了一下通常满足这几个条件的团队最适合用这类自建CRM客户量在几百到几千之间没有到海量客户需要自动化营销的程度业务流程以人工沟通为主微信、电话、线下见面、邮件都有可能团队规模在5到50人不需要复杂的组织架构和跨部门审批流有基础的技术能力或者愿意花一点成本做二次开发如果你的需求比这个再复杂比如要做大规模邮件营销、复杂的报价审批、多币种订单管理我建议还是评估成熟产品。自建CRM的边界要清楚它不是替代Salesforce而是替代“表格微信群个人备忘录用”的那套混乱状态。1.3 核心功能范围项目第一版定的功能范围很克制一共五个模块客户档案统一的客户信息库支持标签、所属销售、自定义字段沟通记录电话、微信、邮件、面谈等多种渠道的互动时间线跟进任务针对客户或商机的待办提醒支持截止时间和负责人销售漏斗按阶段统计商机数量、金额和转化情况权限管理基于角色的数据权限销售只能看自己和本团队的数据管理员看全部这里有一条经验想分享做内部系统第一版功能一定要做减法。我见过很多项目一上来就想做公海池、批量导入、自动化工单结果半年都上不了线。先解决最痛的“漏跟客户”和“信息不沉淀”后面再迭代才有说服力。2. 数据模型与架构设计2.1 数据模型是整个系统的地基做CRM最重要的不是界面而是数据模型。数据模型定得不合理后面每一个功能都会别扭。DeskcommCRM的核心实体有七个用户、团队、客户、联系人、沟通记录、商机、跟进任务。一眼看上去好像跟别的CRM差不多但我在设计时特别强调“客户”和“沟通记录”这两张表的关系。常规管理系统里客户信息通常是一张静态表字段多到令人发指什么“客户类型”“行业”“规模”“来源渠道”全都怼进去。我的做法是拆成两层一层是客户主档存相对稳定的属性比如公司名称、所属行业、客户等级另一层是动态行为也就是沟通记录。前者是主数据后者是事实数据。这样设计的好处是你统计“这个月新增了多少客户”和“这个月发生了多少次有效沟通”可以走完全不同的数据路径互不干扰。客户主档的核心字段我做成这样字段名类型说明idbigint客户IDcustomer_namevarchar(128)客户名称industryvarchar(64)所属行业customer_leveltinyint客户等级1潜在 2普通 3重要 4核心owner_user_idbigint负责销售IDsourcevarchar(32)来源渠道活动/转介绍/线上等statustinyint状态0潜在 1跟进中 2已成交 3已流失remarktext备注created_atdatetime创建时间updated_atdatetime更新时间沟通记录表则是另一个维度的设计它更像流水账字段名类型说明idbigint记录IDcustomer_idbigint关联客户contact_idbigint关联联系人user_idbigint记录的归属人channeltinyint沟通渠道1电话 2微信 3邮件 4面谈 5其他contenttext沟通内容摘要next_contact_atdatetime下次跟进时间created_atdatetime沟通时间2.2 为什么给客户和联系人分开建模很多初级设计会把联系人的姓名、电话、职位直接塞进客户表里一个客户一行记录客户有三个联系人就得搞三行这是典型的误区。客户是一个组织联系人是这个组织里具体的人两者是一对多的关系。DeskcommCRM里联系人表单独存在包含姓名、手机号、邮箱、职位、微信、备注、是否决策人这些字段并且用customer_id关联回客户主档。这个拆法在第一版貌似多造了一张表但后面做沟通记录、跟进任务的时候真香。比如你要给某个客户的采购负责人打电话你能在沟通记录里快速筛出“和谁聊的”你要统计“核心决策人覆盖率”一条SQL就能跑出来。如果一开始图省事后面想拆数据就要写一堆迁移脚本非常痛苦。技术栈方面我最终选了前后端分离的结构前端用Vue 3加Element Plus后端用Java Spring Boot数据库用MySQL 8缓存和定时任务用Redis。这个选择不是因为它最潮而是团队比较熟招聘成本低生态成熟。如果你只有几个人并且更熟悉Python用FastAPI或Django也能做核心是数据模型和业务流程语言只是工具。数据库和缓存的部署我用了Docker Compose写在一份配置里测试机和生产环境直接拉起来省去很多环境一致性的麻烦。2.3 权限模型要一开始就设计好权限是CRM里最容易被低估的东西。没有权限的CRM就像公司里的所有客户资料摊开在桌上谁都能翻销售肯定不愿意把客户信息填进去。DeskcommCRM的权限模型分了三个层级超级管理员、团队主管、普通成员。普通成员只能查看和编辑“自己负责”的客户团队主管可以看本团队所有客户超级管理员看全部。这个模型在代码里怎么落地我的做法是每一张业务表都带有一个team_id字段和owner_user_id字段查询时统一拼上数据权限条件。这里有一条实战经验不要把权限判断分散在业务代码里到处写建议放在统一的查询拦截层或者用一个权限工具类处理否则很快会出现漏写条件导致的数据越权。后面我会专门讲一个因此踩的坑。3. 核心功能模块实现与实操3.1 客户信息统一管理客户信息统一管理是CRM的第一步大家最讨厌的是把过去Excel里的历史数据倒进去。我做了两种导入方式一是手动新增填写基础字段后直接保存二是Excel批量导入。批量导入看起来简单实际上要做三件事才能真正好用模板下载、字段校验、重复检查。字段校验里最容易出错的是手机号和邮箱格式手机号我用了正则加运营商号段校验邮箱用RFC格式的基础校验。重复检查我建议不要一上来就搞“完全匹配”因为Excel里同一个公司可能被写过“某某科技有限公司”和“某某科技”两种名字。第一版可以先做精确匹配加人工确认比如通过“客户名称完全一致”或者“手机号完全一致”来判重命中后在页面上提示用户合并还是跳过。模糊匹配算法可以放到后面迭代我用的是Levenshtein编辑距离加阈值判断准确率一般但能减轻不少人工筛选的工作量。3.2 沟通记录与客户时间轴沟通记录是整个DeskcommCRM最有价值的功能。我做的不是一个个孤立的“跟进记录表”而是给每个客户生成一条完整的时间轴所有渠道的沟通按时间倒序排类似微信朋友圈的时间线展示。这样销售打开一个客户详情页从上往下看就知道这个故事讲到哪了。时间轴的数据来源有三块手动添加的沟通记录、系统自动生成的商机状态变更记录、以及跟进任务完成记录。也就是说哪怕销售忘了手动记录只要他更新了商机阶段或完成了任务时间轴上也会有痕迹。数据汇总我用了一个简单的策略统一查询三张表在内存里合并排序页面上按日期分组显示。客户量在几千时这个方案完全够用不用一开始就搞复杂的聚合表。添加沟通记录的表单我做得非常顺手客户和联系人默认带出渠道用下拉框内容一个多行文本框再加上“下次跟进时间”这个日期时间选择器。保存后同时触发两件事更新客户主档的updated_at如果设置了下次跟进时间就往跟进任务表里插入一条任务。3.3 跟进任务与提醒机制跟进提醒是解决“漏跟客户”的关键。DeskcommCRM的任务表字段设计很简单customer_id、title、due_at、assignee_user_id、status、completed_at。新建跟进任务时默认负责人就是当前操作人管理员可以把任务指派给别人。提醒机制我最初想得很复杂什么提醒一次没看到就升级给主管后来被自己否了。MVP版本只做了两件事第一每天早上9点给每个用户推送当天到期的任务汇总这个用Spring Boot自带的定时任务加Redis锁实现第二任务到期前2小时在站内通知中心发一条提醒。推送到企业的微信或钉钉群是后期加的通过最普通的机器人Webhook就能实现但第一版不做因为站内通知已经可以解决80%的问题。定时任务的代码我简单写一下思路不用上复杂的分布式调度框架单机部署加一个Redis锁足够Scheduled(cron 0 0 9 * * ?) public void dailyTaskRemind() { String lockKey task:dailyRemind:lock; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofMinutes(10)); if (Boolean.FALSE.equals(locked)) { return; } ListTaskVO tasks taskService.findTasksDueToday(); MapLong, ListTaskVO groupByUser tasks.stream() .collect(Collectors.groupingBy(TaskVO::getAssigneeUserId)); groupByUser.forEach((userId, list) - { notifyService.sendDailyDigest(userId, list); }); }Redis锁的作用是防止同一台机器部署多个实例或者重复触发导致发重复通知。虽然我们暂时只有一台服务器但这个习惯要养成因为以后随时可能扩容。3.4 销售漏斗与基础报表销售漏斗我实现得比较轻商机表里有stage字段取值从“初次沟通”“方案确认”“商务谈判”到“赢单”“输单”。列表页按Stage分组展示每张卡片显示商机名称、金额、所属客户和预计成交日期右上角带一个数字角标。主管可以拖拽卡片来改变阶段实际上就是一个状态流转的图形化入口。统计报表这块我用了一个折中的方案实时查询主库而不是引入独立的数仓。原因是当前数据量太小实时查询完全扛得住引入大数据组件纯属过度设计。报表包括四张图新增客户趋势、商机阶段分布、各销售业绩排名、沟通数量排行。前两张用ECharts的折线图和漏斗图后两张用普通的柱状图和表格。我写SQL时踩过一个索引的坑。统计销售排名时一条查询要对orders表做groupBy刚开始amount字段类型是decimal(10,2)索引没建跑一万条数据就要一两秒。后来给owner_user_id和created_at加了联合索引查询直接降到几十毫秒。这个经验特别典型做报表不要上来就搞预聚合先把索引优化做了再说大多数情况下索引就够用了。4. 从0到1落地实录4.1 环境准备与部署项目环境我用Docker Compose搭建目录结构大概长这样deskcomm-crm/ ├── docker-compose.yml ├── backend/ │ ├── pom.xml │ └── src/ ├── frontend/ │ ├── package.json │ └── src/ └── sql/ └── init.sqldocker-compose.yml里我定义了MySQL、Redis、backend、nginx四个服务。开发阶段后端用本地IDEA启动前端用Vite的代理转发请求生产环境统一用Docker Compose启动nginx托管前端静态文件并反向代理后端接口。这里分享一个配置细节MySQL容器一定要挂载数据卷否则容器一重建数据库全没了。我会在docker-compose.yml里专门指定mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: deskcomm_crm volumes: - ./data/mysql:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro ports: - 3306:3306第一次启动时init.sql会自动建库建表省去手动导入的麻烦。用环境变量文件管理密码而不是直接写死在yml里这个习惯也要养成防止配置不小心被推到公开仓库。4.2 核心接口设计与示例前后端对接时接口设计要尽量直观。DeskcommCRM的核心接口我列几个最有代表性的接口方法说明/api/customersGET客户列表支持分页和搜索/api/customers/{id}GET客户详情包含时间轴/api/customersPOST新建客户/api/interactionsPOST新增沟通记录/api/tasks/remind-todayGET今日到期任务/api/opportunities/{id}/stagePUT更新商机阶段/api/reports/sales-rankingGET销售业绩排名以前端新增一条沟通记录为例请求体长这样{ customerId: 1024, contactId: 58, channel: 2, content: 与李经理确认了报价细节对方需要下周内给最终答复, nextContactAt: 2024-03-18 10:00:00 }后端收到请求后的处理顺序是先校验参数再插入沟通记录表然后更新客户表的updated_at紧接着判断nextContactAt是否非空非空则向任务表插一条待办任务最后给当前用户返回操作成功的消息。整个过程在同一个事务里执行避免出现沟通记录保存成功但任务没生成的数据不一致情况。4.3 前端交互的细节处理前端有几个细节我特别想分享。客户列表页的搜索框一定要做防抖否则每次按下键盘都会发一个请求后端接口虽然扛得住但是体验很糟糕。我的做法是用300毫秒防抖停止输入后再搜索实测下来流畅很多。时间轴的展示建议用相对时间而不是绝对时间。页面顶部显示“今天 14:30”而不是“2024-03-01 14:30”。相对时间看起来直观用户扫一眼就知道最近发生了什么。实现方式很简单后端返回完整时间戳前端根据当前时间计算相对偏移并显示鼠标悬停再显示完整日期。商机卡片拖拽的时候有一个容易忽略的问题拖拽结束后要立即显示乐观的UI反馈同时后台异步发送更新请求。如果失败了再回滚到原来的状态并弹出错误提示。不要等到接口完全返回才更新UI那样会有明显的卡顿感。4.4 报表页面的性能优化前面提过报表的索引优化是一次关键性能提升。这里补充一个场景销售排名接口最初是查询所有订单在代码里做合计后来改为一条SQL直接分组聚合代码少了一大截速度也快了。类似的沟通数量排行也改成直接在沟通记录表上按user_id分组计数不再查客户表。另外一个优化是报表缓存。我用了Redis的缓存key是报表名称加日期过期时间设为10分钟。这样即使有多个管理员同时打开仪表盘也不会每次都打到数据库。缓存更新的策略是懒加载有新的访问时发现缓存过期就重新查询回填。对于第一版的CRM这种方式简单实用不用处理复杂的缓存一致性。5. 上线后踩过的坑与排查技巧5.1 时区问题导致提醒不准上线后出现过一次用户投诉早上设置的第二天下班后提醒结果当天半夜就推送了。排查发现是因为后端服务器时区是UTC数据库存的是UTC时间Redis里存的是时间戳前端显示的是北京时间三个地方各玩各的。这个问题我最终的解决方案是全链路统一使用UTC存储仅在接口返回时按用户时区格式化。数据库连接参数里加上serverTimezoneUTC接口层用FastJson配置日期格式化时指定Asia/Shanghai。这样数据库里存的是标准时间展示层按业务地区转换不再出现歧义。5.2 客户重复合并的坑Excel导入之后重复客户特别多。最开始我的合并功能做得太激进点击“自动合并”之后直接保留最近更新的记录把另一条删除。结果有客户的两个联系人其实是两个人合并后联系人表也乱了数据损失惨重。后来我调整了策略合并之前先对比客户名称、联系人数量、商机数量凡是关联数据不一致的都要求人工处理。合并操作做成两步第一“预合并”只是把其中一条的负责人和沟通记录重定向第二“真正合并”才删除冗余客户主档并且在操作日志里留痕。这个流程保守但是安全。5.3 权限漏写导致越权这是我最后悔的注意点之一。客户列表加了权限过滤但是客户详情接口里有个“关联商机列表”子接口忘记加数据权限条件结果普通销售可以遍历商机ID看到所有客户的商机数据。这个问题不是功能没做完而是权限判断没有统一处理。修完这个Bug之后我做了一次全量代码审查把每个需要数据权限的查询都列了一张清单强制要求Mapper层必须有team_id和owner_user_id的条件。把这个判断抽象成一个公共方法后新增查询都会默认调用才彻底堵住这个口子。5.4 大量导入导致接口卡死Excel导入功能一开始是同步执行的一个5000行的文件导入后端要逐行校验、逐行插入接口可能十几秒都不返回。这不是好体验。后来我改成异步导入前端先上传文件后端立刻返回“导入中”状态后台线程池处理导入用户通过任务ID轮询进度。虽然代码量多了一点但体验好很多用户不需要一直转菊花。异步导入的时候还要注意事务边界。我之前是把整个导入当作一个大事务一有错误就全部回滚后来发现100行数据里有一行格式错误用户就得全部重来。改成按批提交每500行一个事务错误行记录在错误报告里用户下载错误报告后修正再重新导入。这样才能做到部分成功部分失败。5.5 常见问题速查表问题现象排查方向解决建议通知没发出去检查Redis锁和定时任务日志确认单实例还是多实例锁时间不能太短页面时间显示差8小时检查数据库连接时区参数统一用UTC存库返回时格式化导入文件报内存溢出解析Excel时一次性加载全部使用SAX方式逐行读取限制单次导入行数销售漏斗金额对不上检查商机金额更新的并发问题使用乐观锁version字段更新时带上客户列表越权审查Mapper查询条件统一数据权限过滤禁止裸SQL6. 给想自建CRM的人一些实在建议DeskcommCRM做到这个版本我心里最大的感受是内部工具的价值不在于功能数量而在于每天是不是真有人用。功能再炫销售觉得录入成本高、查询不顺手最后还是回到Excel和微信上去。所以做完第一版后我花了很多时间观察团队的使用习惯把“最常用的三个页面做到打开即用”作为优化目标而不是急着增加新功能。如果你也想做一个类似的系统我建议先从一张客户表、一张沟通记录表、一个任务提醒开始把它们做扎实再把漏斗和报表一点点加上去。技术选型不要追新选团队最熟的那套框架只是表达业务逻辑的手段。权限模型一定要在一开始就设计好别等数据多了再补那时候改造成本会让你怀疑人生。最后分享一个小技巧上线后我每天都会看操作日志尤其是记录下哪个页面停留时间最长、哪个按钮都没人点。没人点的功能就该砍掉页面里藏着没人用的入口比没有功能更伤体验。CRM这种数据密集的系统简洁就是最好的用户体验保持克制持续迭代比一开始做大量预测性功能靠谱得多。