1. DeskcommCRM项目概览与核心场景
1.1 这个项目要解决的销售管理痛点
先说一个我观察到的现象。很多中小团队用Excel管客户,客户的联系方式、跟进记录、报价历史全塞在一张表里,谁改过、什么时候改的,根本查不到。销售离职带走一批客户资料,老板只能吃哑巴亏。就算上了市面上的CRM,也会发现功能堆得很多,但真正贴合自己业务流程的没几个,还死贵。DeskcommCRM这个项目,最初的出发点就是解决这些实际到不能再实际的痛点。
它不是一个空泛的"客户管理软件",而是把客户档案、沟通记录、商机跟进、任务提醒、数据看板这几件事,用一套清晰的数据模型串起来。核心思路是:所有业务动作都围绕"客户"和"联系人"这两个实体展开,每一次沟通都沉淀为可回溯的记录,每一个商机都挂在具体的客户名下,销售每天该做什么、哪些客户快流失、哪些商机该推进,系统里一目了然。
这个项目适合谁来参考?我觉得有三类人值得看。第一类是中小团队的技术负责人,想低成本搭建一套内部使用的业务系统,不想被SaaS厂商绑定;第二类是正在学习全栈开发的工程师,想找一个业务逻辑完整、能写进简历的项目练手;第三类是对CRM选型不满意的业务管理者,想搞清楚一套CRM系统到底应该怎么设计、哪些功能是核心、哪些是花架子。
市面上开源的CRM项目不少,但很多要么过度设计,分层复杂到新人根本看不下去,要么是纯前端Demo,数据全是写死的。DeskcommCRM的设计原则很朴素:每一行代码都有业务来源,每一个页面都有使用场景。不是说功能越全越好,而是梳理清楚业务优先级,把高频使用的功能做到顺手。
1.2 功能边界与核心模块划分
我把整个系统划分成了四个核心模块,这个划分基于一个原则:销售每天的工作流是什么,系统就怎么组织。
第一块是客户管理。包括客户列表、客户详情、联系人子表、自定义标签和字段。客户列表要支持多条件组合筛选,比如按行业、地区、来源渠道、最近跟进时间筛选,还要支持视图保存,方便不同销售保存自己的常用筛选条件。客户详情页是信息聚合的中心,右侧要能看到这个客户所有的沟通记录、商机、待办任务。这里有个细节:联系人必须有独立的表结构,因为一个客户公司可能有多个联系人,采购、技术、财务说话分量不一样,混在客户表里后面做商机关联时会非常被动。
第二块是沟通与跟进。每一次打电话、发微信、面谈,都要能快速记录,形式可以是文字备注,也可以是结构化字段——沟通方式、沟通对象、下一步计划。这个模块最大的价值在于"可追溯",当销售休假或离职时,接手的同事打开客户详情页,能清楚地知道历史沟通到哪一步了,做过什么承诺,客户有什么顾虑。
第三块是商机管理。商机不能是一个孤立的列表,它必须关联客户和联系人,有自己的金额预估、预计成交日期和阶段。阶段看板(Pipeline)是销售管理最直观的视图,把商机从"初步接洽"到"方案报价"再到"谈判签约"分成几个阶段,拖拽调整阶段,系统自动记录变更历史,管理层能一眼看出哪个环节卡住了。
第四块是数据看板与权限体系。给老板和管理者看的数据,贵在直观,不贵在花哨。个人看板显示自己的客户量、跟进次数、本月商机金额;团队看板显示每个销售的转化率和新增客户数。权限体系则按"普通销售只能看自己数据"这个默认原则来做,管理者可以看到下属的数据,系统管理员拥有全部权限。
2. 技术选型的考量与实际落地
2.1 后端框架与语言的权衡
关于技术选型,这里很容易犯纠结症。我给出的方案是按照团队的长期维护成本来定。如果团队以Java为主,Spring Boot是不二之选,生态成熟、招人容易、资料多到看不完;如果团队人少、追求快速迭代,Node.js或者Python Django都不错。
DeskcommCRM我选择了Java + Spring Boot的组合,不只是因为生态,更因为CRM这类业务系统对事务性和数据一致性要求天然就高。客户资料、商机阶段、沟通记录只要涉及写操作,就必须在事务保护下完成,不能让数据写到一半崩了,产生脏数据。Spring Boot的声明式事务用起来极其顺手,在Service方法上挂一个@Transactional注解,就可以放心做多表联写。
另外,Spring Boot自带的数据校验、依赖注入、AOP能力,让权限拦截和操作日志这类横切需求实现起来非常干净。举个例子,记录操作日志这事,如果每个Controller方法里手动写一遍日志代码,会写到手抽筋,用AOP做一个切面,统一拦截Controller层的请求参数和返回结果,按注解标记的模块名写入日志表,代码量能省掉一大半。
有人问,能不能用Node.js或者Go?当然可以。但这里有个现实问题:后续想加功能、修Bug,能维护这套代码的人好不好找?Java的社区体量决定了,即使核心开发者中途离开,随便拉一个Java工程师都能快速上手。技术选型从来不只是技术问题,更是团队管理的策略问题。
2.2 数据模型设计的几个关键决策
数据库我用MySQL 8.0,InnoDB引擎,原因不多说,稳定可靠、运维成本低、团队熟悉。表结构设计上,有几个关键决策值得展开讲。
客户和联系人必须分开成表,这是必须坚持的底线。客户表存公司级别的信息:公司名称、统一社会信用代码、所属行业、地区、以及比较容易变动的联系方式——这里有个隐藏问题,同一个客户的联系方式可能会换,直接改客户表会导致历史记录里的联系方式对不上,所以我额外加了一张客户联系人历史表,做变更记录。联系方式改成什么不重要,重要的是为什么改、什么时候改的,这一点很多团队不会想到。
商机表要单独建立,并关联客户ID、联系人ID、负责人ID。金额、阶段、预计成交日期这三个字段要允许为空,因为一个商机刚录入时,销售可能只有模糊的意向,还没有正式的报价。以往有些CRM系统设计得不灵活,必填字段太多,录入慢,销售就会抗拒使用。
沟通记录表的设计我特意做宽松。核心字段是客户ID、联系人ID、沟通方式、内容摘要、详细记录、负责人ID、下次跟进时间。内容摘要用一句话概括沟通结论,详细记录放完整的对话要点。为什么要冗余这个摘要字段?因为在列表页和看板上只显示摘要就够了,关联查询详情的成本低很多。下次跟进时间单独建索引,因为这是待办提醒的核心过滤条件。
数据模型想清楚之后,写SQL建表只是半天的事,真正花时间的是在设计阶段反复推敲字段的边界、可空性、关联关系。这一环节跳过了,后面写代码时改表的代价会成倍放大。
2.3 前端与交互层面的取舍
前端技术栈选了Vue 3 + Element Plus,配合Vite做构建工具。选Vue而不是现在风头正劲的React,理由很简单:国内团队对Vue的熟练度普遍更高,Element Plus的中后台组件开箱即用,表格、表单、弹窗、分页这些CRM系统高频使用的组件,样式统一、交互规范,不需要前端团队花大量时间去调UI细节。
页面设计上,我刻意做了几个取舍。客户列表不做无限滚动,用传统的分页方式,因为销售在列表上的核心动作是"找客户",配合右侧的筛选栏和顶部的搜索框,分页浏览是最符合心智的交互。客户详情页不做Tab页堆叠,用左右栏布局——左侧是客户基本信息、联系人、标签,右侧是沟通记录时间线、商机列表、待办任务。这样一屏就能看到关键全貌,不需要来回切Tab。
商机看板用拖拽交互,这就离不开前端框架对拖拽事件的原生支持了——Vue 3结合sortablejs库实现。拖拽改变阶段时,顺手调用接口更新阶段字段和变更记录,前端做乐观更新。用户拖过去立刻看到位置变了,不用等接口返回再刷新,体验好很多。如果接口失败再回滚UI,并给出提示。
权限控制在前端也要做一层配合。后端接口当然要做鉴权,但前端菜单和按钮也要根据用户角色动态渲染,否则普通销售点开"团队数据"菜单,拿到403错误,体验很差。前端路由配置里用meta信息标注每个路由需要的角色,路由守卫里做拦截,菜单按权限过滤生成。
3. 核心功能模块的实现细节
3.1 客户档案模块:从混乱到有序
客户档案是整个CRM的心脏,这个模块做得不好,其他功能再好也是空中楼阁。
客户列表页,我做了三个核心筛选维度:所属销售、最近跟进时间范围、客户状态。查询用的是动态SQL拼接,MyBatis-Plus的LambdaQueryWrapper很方便,条件非空才追加查询条件。列表默认按最近更新时间倒序,因为销售关心的是"最近有没有人跟进过这个客户"。
这里有个性能细节要注意。客户列表的查询不能只查客户主表,还需要关联统计商机总额、最近跟进时间、待办任务数、联系人数量。如果每条记录都子查询,数据量大了就会很慢。我的做法是用MySQL的窗口函数,一次关联查询把客户主数据和统计结果打平成一张临时结果集,再套一层分页查询。在客户量十万级以内,这个方案的性能完全够用,SQL看上去稍微复杂一点,但比ORM层层嵌套查询高效得多。
客户详情页要聚合多类数据,我用一个聚合服务接口一次性返回,前端不用分别拉多个接口再拼装。接口内部的逻辑是分别查客户基本信息、联系人列表、沟通记录、商机列表、待办任务,然后组装成一个大DTO返回。这样处理后端虽然多几行代码,但对移动端弱网环境友好很多。就我实测的结果,聚合接口一次请求的耗时稳定在100ms以内,这个性能对用户体验来说足够好了。
客户导入功能是很多业务口必提的需求。Excel导入用EasyExcel库,先在后端解析模板,校验必填字段,再逐行落库。校验失败的记录收集起来,错误信息写在一个List里,前端下载错误报告Excel。这里要特别注意导入的幂等性:同一批文件重复提交,必须在导入开始前做重复性校验,否则销售导两次Excel,客户资料就翻倍了。我用客户公司的统一社会信用代码作为业务唯一键,存在即跳过或更新,由用户在前端选择策略。
3.2 沟通记录模块:记录本身就是生产力
沟通记录如果做得太繁琐,销售会不愿意填。所以我定的原则是:"三步之内完成记录"。详情页右侧时间线顶部,一个大输入框,选择沟通方式、填写摘要、详细记录、点保存,这三步完成一次记录。
这里有个小设计:沟通方式我用枚举固定了几种,电话、微信、邮件、面谈、其他,不允许自由输入,目的是后续做统计时可以按方式分组。比如管理层想看"这个月电话沟通了多少次",统计起来就非常快。如果允许自由输入,会出现"微信""vx""WX"各种写法,统计就废了。
详细记录里我支持了@联系人的功能,这样在这条沟通记录中关联具体联系人,后续商机关联时能快速找到关键联系人。@的实现在前端是一个下拉选人组件,存的是联系人ID列表,展示时渲染成标签样式。
每次沟通记录保存时,后台还会做一件事:更新客户表的最近跟进时间和跟进状态。这个动作放在同一个事务里执行,保证沟通记录出现时,客户列表的最近跟进时间同步变化。不这么做,就会出现"沟通记录里明明记录了,客户列表却显示很久没跟进"的尴尬。
下周跟进时间到期时,系统通过定时任务——我用的是Spring自带的@Scheduled注解,每分钟扫一次,把到期记录插入当天的待办提醒表里。销售登录系统后首页就会显示"今天需要跟进客户"的列表。这里用到了XXL-Job这样一个分布式任务调度平台吗?没有。单机部署的场景,Spring的定时任务够用了,不要再引入额外组件增加运维成本。等到真的要水平扩展部署多实例时,再考虑分布式锁方案。
3.3 商机与跟进流程:把销售动作标准化
商机的核心不在于录入几个字段,而在于"阶段流转"的可视化和可追踪。
我把商机阶段定义为:初步接洽、需求确认、方案报价、谈判签约、赢单、输单。前四个是进行中的阶段,赢单和输单是终态。每个阶段可以设置进入条件,比如"方案报价"这一阶段要求该商机名下至少有一条报价记录,否则不允许拖拽进入。
商机阶段变更通过一个独立的接口完成,不要在列表页的接口里顺手做。这个接口做的事:更新商机阶段字段、记录阶段变更历史、判断是否进入赢单/输单终态并触发后续动作——赢单时更新关联客户的状态为"合作中",输单时写一条总结原因字段。
商机看板的实现:按阶段维度分组查询商机列表,前端按阶段字段分列渲染。拖拽时调用changeStage接口,数据库更新成功后,前端把商机数据从原列表移到新列表。商机的金额合计,每个阶段列顶部实时显示,销售管这个叫"管线值",一眼看清各阶段金额分布。
商机的金额字段我设置成Decimal(12,2),单位是元。这里可能有人问,金额那么大,为什么不用int存分?说实话,CRM这种系统金额准确到分不会出错,但直接用元可以让报表和导出更直观,不会出现Excel里显示450000.00这种让业务同事摸不着头脑的情况。至于并发下金额被覆盖这类问题,靠乐观锁version字段防住就行。
3.4 报表与数据看板:让管理层看得懂才是目标
数据看板这个模块,如果只是把列表数据换几个图表样式展示一遍,那就不叫看板,叫摆设。
Dashboard页面我设计了三个区块:顶部是汇总指标卡,展示总客户数、本月新增客户数、本月新增商机金额、本月赢单金额;左侧是趋势图,展示最近六个月的商机金额变化趋势;右侧是阶段转化漏斗,展示各阶段商机数量和金额的漏斗图。
这些数据背后的SQL,核心思路是聚合查询。比如本月新增商机金额,就是sum(amount) where create_time between月初 and 月末 and status not in (输单)。趋势图则按月份分组求和,MySQL的DATE_FORMAT函数配合GROUP BY month就能出结果。
数据响应速度方面,在数据量达到几十万条时,实时聚合查询会开始感觉到压力。我的优化方案是引入一张日汇总表,每天凌晨定时任务把前一天的数据按维度和指标聚合好,查询看板时直接读汇总表,响应时间从几秒降到几百毫秒。报表这种场景,对实时性要求没那么高,只要今天能看到昨天的数据就行,没必要实时全量汇总。
权限处理上,个人看板查询条件强制带上当前登录用户ID,团队看板校验角色必须是经理或以上,否则直接拒绝。这个判断在Service层做,不要只靠前端路由守卫,后端才是最终防线。
4. 实操部署与上线配置
4.1 环境准备与初始化步骤
一套完整的DeskcommCRM部署,需要准备的内容其实不多,核心就三样:JDK 17、MySQL 8.0、Redis 7。Redis在这里用来做登录态的会话存储和接口访问频率限制。
初始化步骤我列个标准流程:
- 安装JDK 17并配置JAVA_HOME环境变量,命令行输入java -version能输出版本号即可。
- 安装MySQL 8.0,设置root密码,创建业务数据库crm_db,字符集选utf8mb4,排序规则选utf8mb4_unicode_ci。
- 执行项目根目录下的schema.sql脚本,初始化数据表结构和基础数据。基础数据包括系统管理员账号、角色的数据字典(字典类型的枚举值等)。
- 修改application.yml里的数据库连接、Redis连接配置。
- 在前端项目根目录执行npm install安装依赖,npm run dev启动开发模式;生产环境用npm run build打静态包,然后通过nginx部署到服务器。
这五步走完,系统就能跑起来了。如果是纯本体验证,可以直接用docker-compose把MySQL、Redis、后端接口、前端静态页一键起起来,相关配置我放在deploy目录下,按自己的环境调整IP和端口就能用。
想要更简单的方式,就用项目里带的Dockerfile分别构建后端和前端镜像。后端镜像以eclipse-temurin:17-jre为底,把打好的jar包复制进容器,暴露8080端口。前端镜像用nginx:alpine做静态资源服务,把dist目录放到/usr/share/nginx/html下,再写一份default.conf配置反向代理——把/api前缀的请求转发到后端容器的8080端口。这套Docker化方案在生产环境的资源占用非常少,整个系统跑起来内存占用大概在1GB左右,一台2核4G的云服务器带起来绰绰有余。
4.2 权限与初始账号配置
系统第一次登录用的是内置的管理员账号。这个账号的密码是随机生成的,部署时打印在日志里,首次登录后强制要求修改。这个设计我不要用默认密码admin/123456,那种做法第一次上线就有安全风险。
角色的数据模型我做成RBAC模型,用户、角色、菜单三层。菜单表存的是前端的路由路径和按钮标识,角色菜单关联表决定某个角色能看哪些菜单和按钮。管理员账号默认拥有全部权限,可以新建角色、给角色分配菜单权限、把用户挂到角色下。
这里有一个容易踩的坑:权限数据如果变化频繁,每次请求接口都去查数据库权限信息会很浪费。我的做法是在用户登录成功后,把用户的权限标识列表放进Redis,设置两小时过期。接口鉴权时先从Redis拿权限标识,Redis没有再去数据库查并回填。这样权限变更最多两小时后生效,对CRM这类内部系统来说完全够用。
还有一种分布式会话方案是JWT无状态token,但JWT有一个安全隐患——服务端无法主动让一个已签发的token失效。员工离职了,他的token在过期之前仍然有效,这对于业务系统是不能接受的。所以我选择了Redis存储会话,服务端随时可以删除指定用户的会话记录,强制下线。
4.3 上线的数据迁移与初始化策略
上线最怕的不是代码有Bug,而是历史数据迁移得乱七八糟。这里我分享一套实操策略。
旧Excel数据导入新系统,分三步走。第一步,整理数据模板,和业务方确认哪些列是必填、哪些列要清洗。第二步,写一个一次性迁移脚本,用JDBC批量读取Excel,按客户表、联系人表、商机表的顺序逐批导入,每批次500条包一个事务。为什么要按表顺序?因为有外键关联,客户ID不存在时,附件联系人记录挂不上去,必须先保证主记录落库拿到ID。第三步,迁移完成后跑数据校验SQL,核对客户总量、商机金额总和,输出核对报告。
历史沟通记录的迁移要小心。Excel里的沟通记录往往没有规范的负责人信息,无法挂到具体销售名下。我的默认策略是挂到"系统管理员"名下,保留原始的时间和内容,标注"历史迁移数据",避免出现一条记录都没有负责人这种脏数据。
上线切换那天,要和业务方明确一个操作纪律:旧Excel表停止更新,所有客户数据从CRM里看。否则两边并行维护,第三天就会数据不一致,接下来就是销售和管理层对系统信心崩盘。这件事比技术方案重要得多,它决定系统是否能真正活下来。
5. 常见问题排查与避坑经验实录
5.1 并发编辑冲突与数据一致性
多销售同时编辑同一个客户时,出现"最后写入覆盖"问题是大概率事件。A销售改了手机号,B销售同时改了地址,A先保存,B后保存,A的手机号修改就被覆盖了。
解决思路是在客户表加一个version字段,保存时检查当前版本号是否与读取时一致,不一致就拒绝更新,前端提示"该客户信息已被他人修改,请刷新后重试"。version字段用乐观锁实现,代价低,对CRM这种并发冲突不算特别频繁的场景足够用了。
我实际遇到的冲突场景比这个更隐蔽:沟通记录保存后更新客户最近跟进时间,两个销售同时对同一客户提交沟通记录,各执行一条update语句后提交,MySQL的行锁会让后者的更新等待前者提交。这时候如果事务里还有别的大查询,非常容易出现锁等待超时。排查时看MySQL的锁等待日志,通常都能定位到事务没及时提交或者查询没走索引,把索引补上就好。
5.2 搜索性能瓶颈与索引设计
CRM系统最常见的搜索场景是"根据客户名称模糊查询"。一开始我直接在客户名称字段上建了普通索引,结果数据量上到十万条时,用%关键字%模糊查询依然很慢,走了全表扫描。MySQL的普通B+树索引不支持以通配符开头的LIKE查询,我建的索引根本没被用到。
解决方案有两个,按数据量来选。十万级以下是主流的方案,用前缀匹配查询,索引能命中,但前提是用户记得客户名称的前几个字;真正的企业级解法是引入Elasticsearch做全文检索,客户名称、联系人姓名、手机号等都同步到ES里,搜索接口改成查ES,毫秒级返回。但对于DeskcommCRM这个规模,引入ES确实重了,我的折中方案是:客户名称支持拼音首字母搜索,在客户表加拼音索引字段,查询时用首字母前缀匹配,配合普通索引,实测性能提升非常明显,销售用起来也顺手。
5.3 权限漏配与越权访问的隐藏风险
权限这个环节最容易出问题的不是权限校验逻辑写错了,而是漏了。某些查询接口忘了加当前用户数据范围的限制,销售查一下别人的客户,数据就露出去了。
我踩过这样的坑:导出客户Excel的接口当时只做了登录校验,没做数据范围校验。结果出现了普通销售导出全公司客户数据的情况。虽然不是恶意,但暴露了一个事实——后端接口的权限校验必须有统一的规范。我在项目里加了一个基于AOP的权限切面,用注解标记接口需要的数据范围,方法执行前自动把当前用户的ID拼进查询条件,从根上解决漏配问题。
另外,前端按钮级的权限控制也要同步做。不是登录了就能看到所有功能按钮,比如"删除客户"按钮只有管理员角色才显示,普通销售根本没有入口。后端接口层再校验一遍,前端控制不了数据泄露的风险,后端才是安全的底线。缩进式的安全设计,宁可重复校验,也不要裸奔。
5.4 数据准确性与重复记录问题
CRM运行几个月后,重复数据往往成为管理层的最大痛点。同一个客户被两个销售录了两笔,跟进的商机金额统计时就会翻倍,看板数据失真。
预防手段在录入环节就要做:录入客户名称时,前端调用"查重接口",返回相似名称的客户列表,提示用户确认是否已存在。但这是预防,存量数据还是要清理。我的做法是把全库的客户名称做分组,用归一化处理——去掉空格、去掉公司后缀词——然后找重复组,生成一份《重复客户清单》给管理员确认,人工确认后才能合并。合并操作确认后要把两个客户的商机和沟通记录迁移到保留的那条记录上,同时把被合并的客户标记为"已合并"状态,防止后续再被关联。
这个合并操作我必须用事务包起来,而且是整个系统里最大的一个事务。执行前先备份表数据,因为一旦误操作,几百条记录被误合并,恢复成本极高。
6. 项目复盘与几个值得思考的教训
如果在写代码之前能再多花一周去梳理业务流程,沟通记录的表结构可以设计得更符合实际需要。比如记录类型的分类,我一开始只分了电话、微信、邮件、面谈四类,结果实际业务里客户还会发合同评审、对账单这些附件,这些其实也应该属于"沟通"的范畴。后来在实现中补了一个"附件"子表才解决,早期如果看清这一层,就不用后面返工了。
Deployment的经历让我最深的感觉是:CRUD系统本身不复杂,复杂的是业务规则和边界条件的梳理。比如"同一个客户的重复录入如何发现""商机从报价阶段进入谈判阶段要满足什么条件""没有负责人记录的商机怎么处理"——这些问题在代码层面都不难写,难的是在写之前有意识地意识到这些问题,并找到合适的解法。
还有一点,权限和数据隔离必须在设计阶段融入数据模型,而不是开发完后补安全模块。DeskcommCRM的每个业务表都带上owner_id字段,就是这一思路的体现。后期再做权限隔离,SQL改写和逻辑判断会散落在代码各个角落,维护成本非常高。
从第一行代码到稳定上线,整体大概花了三周时间。前端用了Vue 3和Element Plus,后端是Spring Boot,中间穿插了数据模型设计、接口联调和部署测试。在速度上并不算快,但我个人觉得,能够明明白白讲清楚一行代码对应什么业务场景,这个系统才算是真正被团队所用,而不是一个摆设。
这个项目后续如果再扩展,我觉得可以往两个方向走。一个是增加邮件、短信渠道的自动归档推送,沟通记录做到"零手动录入";另一个是加入简单的赢率预测模型,根据历史商机的阶段、金额、行业等因素,给销售一个成交概率参考。不过这些都是后话了,先把基础打牢,核心功能真正被业务方用起来,比什么都重要。