ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从零开发企业内部CRM系统:技术选型、权限设计与性能优化实战

从零开发企业内部CRM系统:技术选型、权限设计与性能优化实战 1. 先说清楚DeskcommCRM 到底解决什么问题我第一次接触 DeskcommCRM 这个项目的时候团队里其实已经有一套“用 Excel 管理客户”的流程了。听起来很离谱对吧但小团队、销售型公司、初创项目这类场景里 Excel 管理客户反而是常态。问题在于客户一多Excel 就撑不住了。重复客户记录、跟进到哪一步没有统一口径、销售之间互相撞单、管理层想看个漏斗数据还得等助理手工汇总。我接手 DeskcommCRM 的初衷就是把这些乱七八糟的“人肉 CRM 流程”换成一套能真正跑起来、还不至于过度设计的系统。DeskcommCRM 的定位很简单一套面向中小型销售团队和客户成功团队的桌面端通信型客户管理系统。它不是一个什么都能干的 SaaS 巨无霸而是把客户资料、线索跟进、销售过程、数据统计这几件最核心的事做到位。它能解决的核心问题有三个一是客户信息不再散落在每个人的电脑里而是统一沉淀在系统里二是每个销售每天应该跟进谁、跟到哪一步、下一步该做什么系统能给你一个清晰的提示三是管理者不需要再等周报月报打开仪表盘就能看到线索到签约的转化情况。这篇内容适合谁看如果你正准备做一套企业内部管理系统不管是 CRM、工单系统还是其他带权限、带审批的业务系统这篇文章里关于数据模型、权限设计、统计口径、部署实践的思路都可以直接借鉴。如果你只是选型想了解自研 CRM 要踩哪些坑这篇文章也能帮你提前排排雷。2. 整体设计思路与技术选型拆解2.1 功能模块划分先把范围边界切清楚做任何系统第一步不是写代码而是把边界切清楚。DeskcommCRM 第一版我规划了五个核心模块客户管理客户的基本信息、联系人、所属销售、客户来源、分类标签。线索管理还没转化为客户的潜在名单需要录入、分配、跟进、转化。跟进记录每个客户每条跟进的时间、方式、内容、下一步计划。商机管理把一个客户从“初步接触”到“赢单/输单”的过程拉成一条看得见的管线。数据报表销售漏斗、个人业绩看板、客户来源分析、跟进效率统计。这个划分看起来不复杂但有一个很重要的原则单模块职责闭环模块之间只通过明确的数据关系连接。比如线索“转化”为客户本质不是复制一份数据而是把线索状态改成“已转化”同时创建一条关联的客户记录并且保留线索和客户之间的双向索引。这样做的理由是你永远能追溯一个客户到底是从哪条线索来的而不是一脸懵地看到一堆“来路不明”的客户。还有一个容易被忽略的模块是“系统管理”。权限、角色、字典、操作日志、数据导入导出这些非业务功能占了整个项目大概三分之一的工作量。别小看它很多 CRM 项目做到一半烂尾就是低估了这一块。2.2 技术栈选型为什么我最后选了这套组合技术选型没有绝对的标准答案只有“适不适合你的团队”和“适不适合你的部署环境”。DeskcommCRM 的技术栈我定的是后端 Java Spring Boot MyBatis-Plus数据库 MySQL 8.0缓存 Redis前端 Vue 3 Element Plus。选 Java Spring Boot 的理由很现实企业内部系统最怕的是“人走了代码没人能接”。Spring Boot 生态大、招人容易、资料多哪怕几年后维护的人换了也不至于对着代码抓瞎。相比 Node.js 或 Python 后端Java 在这种业务系统里的稳定性和事务处理能力也更让人放心。MyBatis-Plus 则是为了效率——它能在不影响 SQL 灵活性的前提下把单表 CRUD 的代码量砍掉一大半。MySQL 做主力存储没什么好争议的CRM 的数据量级通常在千万以内MySQL 完全扛得住。Redis 主要用来做三件事登录令牌存储、热点数据缓存比如部门架构、字典项、并发场景下的分布式锁。前端选 Vue 3 Element Plus 主要是考虑后台管理界面的开发效率。Element Plus 的表单、表格、弹窗组件很成熟能让我们把更多精力放在业务逻辑上而不是自己造轮子。提示如果你的团队是纯前端团队后端也可以用 Node.js 或直接上低代码平台。但我个人经验是带事务、带复杂权限的业务系统用 Spring Boot 这类成熟后端框架踩坑成本最低。2.3 数据模型设计一套能撑住后续扩展的表结构数据模型是整个 CRM 的心脏。我见过太多项目因为表结构设计太随意做到一半发现加字段加不进去、统计查不出来、权限限制不住。DeskcommCRM 的核心表设计我遵循了几个原则。第一客户表customer只存客户本身的属性不掺和业务状态。客户名称、行业、规模、地址、来源、所属销售、创建时间、更新时间。有人喜欢把“跟进状态”也放在客户表里我强烈不建议这么做。因为一个客户可能有多个商机商机和商机之间的状态是不一样的。把所有状态都堆在客户表上最后只能给你数据统计埋雷。第二跟进记录表follow_up_record一定要冗余客户名和销售名。虽然通过外键关联能查到但在列表页和导出报表时如果没有冗余字段每一个列表都要 join 三四张表性能会很难看。冗余字段的代价是数据一致性需要自己保证但在这个场景下收益远大于成本。第三商机表opportunity里放了一个 stage 字段用字典值表示阶段而不是直接存中文。比如 1 表示初步接触2 表示需求确认3 表示方案报价4 表示谈判5 表示赢单0 表示输单。这样做的好处是前端可以配置颜色和文案后端统计漏斗时可以直接按数字分组不用处理中文字符串的匹配问题。第四所有核心表都加了 is_deleted 字段做逻辑删除。这看起来是个老生常谈但真的很多项目第一版没做后来要找回误删数据的时候傻眼了。逻辑删除配合 MyBatis-Plus 的 TableLogic 注解实现成本极低千万别省。我把客户表结构简化贴出来供参考CREATE TABLE customer ( id bigint NOT NULL AUTO_INCREMENT, customer_name varchar(200) NOT NULL COMMENT 客户名称, industry varchar(100) DEFAULT NULL COMMENT 所属行业, customer_source varchar(50) DEFAULT NULL COMMENT 客户来源, owner_user_id bigint DEFAULT NULL COMMENT 负责人用户ID, owner_dept_id bigint DEFAULT NULL COMMENT 负责人所属部门ID冗余, phone varchar(50) DEFAULT NULL COMMENT 联系电话, address varchar(500) DEFAULT NULL COMMENT 地址, remark text COMMENT 备注, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted tinyint NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_owner_user_id (owner_user_id), KEY idx_customer_name (customer_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户表;这里我把 owner_dept_id 单独冗余出来了。这不算规范化的设计但在做数据权限控制时非常有用——它可以避免每次判断“某个部门的销售能不能看这个客户”都要连到用户表再连到部门表性能提升是实打实的。3. 核心模块落地实操3.1 客户录入与线索去重不重复才有可信度CRM 系统最怕什么不是功能少是数据烂。客户录入了两遍、三遍同一个公司被不同的销售各抢一遍最后系统里看起来客户量很大实际上真实客户数量少得可怜所有统计数字都是虚的。所以客户管理的第一个硬功能就是去重。DeskcommCRM 的去重策略分两层。第一层是录入时实时校验。在客户名称输入框失焦时前端把客户名称传给后端后端使用精确匹配和关键字匹配双重判断。精确匹配就是完全同名直接提示“已有同名客户”关键字匹配则稍微复杂一点比如“北京字节跳动科技有限公司”和“字节跳动”这种靠简单 SQL 的 LIKE 是查不出来的我用的方案是维护一个模糊匹配规则把一些常见后缀“有限公司”、“股份有限公司”、“科技”、“技术”做归一化处理后再按归一化后的名称查重。第二层是导入时的批量去重。Excel 批量导入的场景逐条提示是不现实的我的做法是上传后先在后台异步跑一遍去重任务生成一个“疑似重复客户列表”前端展示给用户勾选——哪些是新增哪些是跳过。这一步看着简单但它决定了导入功能能不能被用户真正用起来而不是导一次骂一次。3.2 跟进记录与时间线设计别把客户历史变成一条流水账跟进记录是 CRM 里使用频率最高的功能也是体验最容易被做烂的功能。如果你的系统里跟进记录只是按时间从上往下排的一堆文字销售根本不想用因为他很难在一屏之内快速搞明白“这个客户之前聊了什么、卡在哪了”。DeskcommCRM 的跟进记录做成了“时间线”样式。每条记录包含跟进方式电话、微信、拜访、邮件、跟进内容、下次跟进时间、下一步计划。时间线中间有一条主轴线客户的关键动作比如“创建商机”、“报价发送”、“赢单”会以特殊标记插入到时间线里。这样销售打开客户详情页从上往下滑动就能像看故事一样看到这个客户的完整生命周期。这里有个技术细节值得一说跟进记录的时间线查询如果只是单纯按时间查询 follow_up_record 表那客户“创建商机”这类非跟进行为就没法出现在同一条时间线里。我最终用的是类型加时间的混合查询——把跟进记录、状态变更记录、文件上传记录合并成一个视图数据源再在内存里按时间排序分页。数据量不大时这么干完全没问题但数据量超过十万条后建议在数据库层做 union 查询或者引入搜索中间件避免每次都把数据拉到内存。还有一个非常容易被忽略的点跟进计划。每一条跟进记录里都有一个“下次跟进时间”但这个计划如果只是展示出来用户是不会自觉遵守的。DeskcommCRM 做了一个“今天待跟进”的首页清单每天上班打开系统销售第一眼看到的就是今天该跟进的客户列表。这一招非常管用它把“计划”变成了“任务”使用率立刻上来了。3.3 报表与销售漏斗统计口径不对报表就是害人报表模块是整个项目里最考验逻辑严谨性的地方。同样是“本月新增客户数”你可以理解为“本月创建时间在当月的客户数”也可以理解为“本月首次成为客户的时间在当月的客户数”。两种口径差一点点结果可能差很多。DeskcommCRM 的销售漏斗图我花了不少心思。漏斗的阶段和商机表的 stage 字段对应统计逻辑是对每个商机取其当前所处阶段然后从最高阶段到最低阶段分别计数。比如“方案报价”这一层的数量就是所有当前处于方案报价及之后阶段的商机数量。为什么要“及之后”因为一个已经赢单的商机一定曾经经历过方案报价这个阶段。如果你只统计当前恰好处于某阶段的商机漏斗越往下数字越小得离谱而且会误导管理者以为“报价后成交率很低”实际上只是大家没有及时更新阶段而已。这个口径问题是所有做 CRM 报表的人都要小心的地方。我建议在设计报表时一定要和业务负责人确认清楚每一个指标的定义并且把定义写在系统帮助文档里。否则系统做出来后每个人都按自己的理解解读报表争论不休那就不是工具而是制造矛盾的机器了。3.4 权限模型销售不该看到别人的客户CRM 的权限模型单说“角色”是不够的。DeskcommCRM 用的是“角色 数据范围”的双层机制。角色管的是“能做什么”比如销售顾问能新增客户、编辑自己的客户、查看报表销售主管能分配客户、查看本部门所有客户管理员能配置系统、操作数据字典。数据范围管的是“能看到哪些数据”我把它划分成四档仅本人、本部门、本部门及下属部门、全部。这个数据范围不是一个开关而是挂在角色上的一个数据字段。用户登录后后端会把当前用户的数据范围计算出来注入到每次查询的过滤条件里。实现上我用的是 MyBatis-Plus 的拦截器机制。自定义一个 DataScopeInterceptor在执行 select 语句时自动根据当前登录人的数据范围装配 SQL 条件。比如“仅本人”就追加AND owner_user_id 当前用户ID“本部门”就追加AND owner_dept_id 当前部门ID。注意数据权限的过滤必须放在后端 SQL 层完成绝对不能依赖前端传参、不能依赖前端隐藏按钮。否则用户直接调用接口就能越权访问数据这是严重的安全漏洞。还遇到过一个实际问题客户被分配给了离职销售的怎么办我的方案是设计了一个“客户继承”逻辑——销售离职后管理员可以把该销售名下所有客户批量转移给其他同事转移的同时保留历史跟进记录并且会在时间线里打一条系统记录“客户负责人由张三变更为李四”。这个功能当时不算起眼但后来人事变动频繁时大家都庆幸有它。4. 部署、性能优化与安全加固4.1 部署方案一套脚本搞定测试和生产DeskcommCRM 的部署我用的是 Docker Compose。一个项目包含四个容器后端应用、前端 Nginx、MySQL、Redis。之所以没有上 Kubernetes是因为团队规模摆在那里一套 K8s 集群光维护成本就够喝一壶了。Docker Compose 在单机部署场景下完全够用配置文件清晰回滚也简单。关键的部署细节有几个。第一MySQL 数据目录要挂载到宿主机否则容器一删数据就全没了。第二后端日志也要持久化排查问题的时候没有日志等于瞎猜。第三Nginx 里要配好前端静态文件的缓存策略否则每次发版后用户浏览器还带着老版本明明功能改好了用户却看不到。4.2 查询性能优化列表页从 8 秒到 300 毫秒CRM 系统性能最大的瓶颈往往不是高并发而是“列表页越查越慢”。客户列表、跟进记录列表这些页面翻到后面几页时数据库要扫描越来越多的数据响应时间直线上升。我在 DeskcommCRM 里做了三件比较有效的优化。第一MyBatis-Plus 的分页插件必须配好并且确认 count 查询走了索引。这是最基础的一步但很多人忽略了 count 的性能表数据一多就崩。第二给常用的查询条件建联合索引。比如客户列表最常见的过滤条件是“所属销售 创建时间”就建立一个(owner_user_id, created_at)的联合索引。字段顺序很讲究等值判断的字段放前面范围判断的字段放后面。这个顺序错了索引效率会大打折扣。第三列表页的“高级筛选”不做全字段 like。有人把客户名称、联系电话、地址、备注四个字段全做 like一旦输入一个通用词比如“科技”数据库直接全表扫描。我的方案是名称和电话走索引查询地址和备注只允许在点击“搜索”时再做包含匹配而且匹配时限制最多返回前 2000 条。这样做虽然损失了一些灵活性但保住了核心体验。实际操作中还有个很典型的案例跟进记录列表默认只查询“最近 6 个月”的数据并且加了一个时间范围控件让用户可以自己选更长的时间段。为什么要默认限制因为大部分用户翻历史记录最多翻到半年前限制时间范围能让索引效率提升一大截用户几乎感知不到“数据变少”。4.3 数据安全与备份敢不敢拍胸脯说数据不会丢做业务系统数据安全永远是红线。我在这块踩过一个让我印象深刻的坑有一版部署脚本把 MySQL 的数据目录挂载错了导致容器重启后所有数据都消失了。幸好是在测试环境如果是生产环境那就不是事故是灾难了。所以后来我只用两种备份方案并且每一条都验证过“确实能恢复”才敢上线。第一是 MySQL 每日凌晨全量备份用 mysqldump 导出 SQL 文件保留最近 14 天的备份第二是每月一次全量备份直接同步到对象存储防止服务器磁盘出问题。这里我想强调一句备份没用恢复才有用。我见过太多人配了备份任务但从没跑过恢复演练真出问题时备份文件是坏的或者恢复步骤忘了最后还是抓瞎。我自己的习惯是每个季度做一次从备份恢复到新环境的演练确认数据完整、服务能起、核心功能可用。5. 踩坑实录常见问题与排查技巧5.1 Excel 导入乱码和丢数据的处理Excel 导入是 CRM 系统最基本的入口之一但我敢说十个系统有八个在这上面出过问题。最早的坑是编码问题用户上传 CSV 文件如果文件是 GBK 编码而我们按 UTF-8 读取中文全部变成乱码。我的处理思路是先读取文件的前几个字节判断编码有 BOM 的就按 BOM 识别没有 BOM 的文字则通过解码成功率来判断是 UTF-8 还是 GBK最后统一转成 UTF-8 再做后续处理。另外不要直接用 POI 的 DataFormatter 对单元格进行原样读取要区分清楚“字符串”和“数字”——客户电话这种字段如果用户输入的是超长数字Excel 内部会将其转成科学计数法或数字格式直接读出来会丢失精度必须在读取时统一转成字符串并去除小数点。我还在导入功能里加了一个“先预览后提交”的环节上传文件后系统解析前几条数据在前端展示成表格让用户确认格式对不对避免全部导入后才发现这一批数据全是乱的。这个功能虽然多花了一点开发时间但大大降低了用户对导入功能的怨气。5.2 并发放慢和死锁问题不是每一次都真的“并发”了有一次用户反馈“保存跟进记录时经常转圈很久”查看日志发现是数据库死锁。死锁的原因说起来有点搞笑——销售在保存跟进记录时系统会自动更新客户表的 updated_at 字段同时也可能自动触发客户表“最近跟进时间”字段的更新。两个请求同时更新同一个客户的不同记录时锁的加锁顺序不一致就死锁了。解决思路有三个。第一所有更新客户表的 SQL都统一先通过客户 id 加锁并且加锁顺序保持一致。第二对“最近跟进时间”这类高频更新字段改成一个异步任务去更新不在请求主链路里同步执行。第三数据库设置合理的等待超时时间让死锁快速暴露而不是无限挂起。这里我想多说一句面对死锁不要一上来就加“全局锁”或“串行化”那样确实能解决问题但会把系统的并发能力直接砍到地板。真正要做的是先理清锁的获取顺序因为死锁的本质就是锁的环形等待。5.3 时区问题凌晨 0 点看到的统计数字对不上第一次部署到服务器后发现报表的“今日新增”数据和实际对不上。排查到最后发现是时区问题服务器用的是 UTC 时间而业务上大家都是北京时间。数据库里存的时间看起来正常但统计 SQL 里用DATE_FORMAT(created_at, %Y-%m-%d)分组时服务器的时区影响了日期边界。解决方式很简单数据库连接的 URL 参数里设置serverTimezoneAsia/Shanghai同时 JVM 启动参数加上-Duser.timezoneAsia/Shanghai然后统一都按北京时间展示。这里我的经验是系统内部存储时间统一用时间戳或 UTC展示和统计统一转成用户所在时区而不要让每一层各转一次那样最容易出偏差。5.4 权限“越权”问题的排查边界条件比主流程更重要权限模块上线前我请测试同学重点做了权限边界测试。结果还真抓到了一个典型问题列表页的权限过滤正常但客户详情页的“导出”按钮可以直接把客户的完整资料导出为 Excel而这个导出接口完全没有做数据权限过滤。也就是说一个“仅本人”角色的用户可以通过导出功能拿到系统里的所有客户数据。这种问题非常隐蔽因为它不在正常的“列表页浏览”主路径上而是藏在“伴生功能”里。只要你新增一个接口而这个接口又在查询客户数据就必须同步考虑数据权限。后来我在代码评审里加了一条硬性要求所有和业务数据相关的查询接口都必须显式声明自己用哪种数据范围不允许存在“无声明”的漏网之鱼。排查权限问题时最快的办法是抓 SQL 日志看看执行的 SQL 里有没有自动追加当前用户的数据权限条件。如果没有那就立刻能锁定是拦截器没有生效还是接口走的是自定义 SQL 绕过了拦截器。写在最后的一点心得项目做到后面我最大的体会是DeskcommCRM 这种内部业务系统难的不是技术而是对业务的理解和对细节的坚持。一个字段的冗余、一个统计口径的确定、一个权限边界的处理看起来都是小事但累积起来就决定了这个系统到底是一个让团队觉得“好用”的工具还是一个被天天吐槽的负担。如果让我重新做一遍我一定不会在功能列表上堆更多东西而是会花更多时间去一线听销售们怎么说话、怎么吐槽、怎么用 Excel 管理客户。系统不是设计师的自嗨它是业务动作的数字化影子影子贴不贴合实体只有跑起来才知道。
返回列表