ARTICLE DETAIL

资讯详情

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

从零构建企业级CRM系统:Spring Boot + Vue 3实战指南

从零构建企业级CRM系统:Spring Boot + Vue 3实战指南 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.0InnoDB引擎原因不多说稳定可靠、运维成本低、团队熟悉。表结构设计上有几个关键决策值得展开讲。客户和联系人必须分开成表这是必须坚持的底线。客户表存公司级别的信息公司名称、统一社会信用代码、所属行业、地区、以及比较容易变动的联系方式——这里有个隐藏问题同一个客户的联系方式可能会换直接改客户表会导致历史记录里的联系方式对不上所以我额外加了一张客户联系人历史表做变更记录。联系方式改成什么不重要重要的是为什么改、什么时候改的这一点很多团队不会想到。商机表要单独建立并关联客户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 沟通记录模块记录本身就是生产力沟通记录如果做得太繁琐销售会不愿意填。所以我定的原则是三步之内完成记录。详情页右侧时间线顶部一个大输入框选择沟通方式、填写摘要、详细记录、点保存这三步完成一次记录。这里有个小设计沟通方式我用枚举固定了几种电话、微信、邮件、面谈、其他不允许自由输入目的是后续做统计时可以按方式分组。比如管理层想看这个月电话沟通了多少次统计起来就非常快。如果允许自由输入会出现微信vxWX各种写法统计就废了。详细记录里我支持了联系人的功能这样在这条沟通记录中关联具体联系人后续商机关联时能快速找到关键联系人。的实现在前端是一个下拉选人组件存的是联系人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中间穿插了数据模型设计、接口联调和部署测试。在速度上并不算快但我个人觉得能够明明白白讲清楚一行代码对应什么业务场景这个系统才算是真正被团队所用而不是一个摆设。这个项目后续如果再扩展我觉得可以往两个方向走。一个是增加邮件、短信渠道的自动归档推送沟通记录做到零手动录入另一个是加入简单的赢率预测模型根据历史商机的阶段、金额、行业等因素给销售一个成交概率参考。不过这些都是后话了先把基础打牢核心功能真正被业务方用起来比什么都重要。
返回列表