SpringBoot+Vue+MyBatis+MySQL CRM系统源码实操拆解与二次开发指南
发布时间:2026/10/13 10:44:05 锦皓数字建站

后台经常收到类似的问题CRM客户关系管理系统源码是不是拿来就能跑是不是改改Logo就能上线说实话看到抱着这种心态来找SpringBootVue源码的人我一般都会先泼一盆冷水。拿这套【基于SpringBootVue的客户关系管理系统配上MyBatis和MySQL】来说我前后完整走了一遍从拿到初始工程到最终在测试环境稳定运行中间踩掉的坑比功能本身多得多。它不是什么高不可攀的大型分布式系统但恰恰是这种“看起来很简单、business逻辑却绕来绕去”的项目最考验全栈功底。这篇文章我不会给你贴一长串的广告式截图我以实际开发者的视角把这套系统的技术选型逻辑、数据库设计、权限控制思路、前后端联调细节、以及那些文档里不会写的坑全部拆开。适合两类人一类是想在中小企业内部做销售过程管理的研发另一类是正在找全栈项目练手、想真正看懂CRM业务流程的学生或初中级工程师。你能从中得到的不只是“代码能跑”而是“知道每段代码为什么要这么写”。1. 先想清楚业务边界CRM不是“客户存通讯录”那么简单1.1 市面上的“通用CRM”和实际业务之间的巨大落差刚拿到这个项目标题时我的第一反应是这又是一个标准的Java全栈练手项目吧无非就是用户登录、客户增删改查、跟进记录顶多再来个图表统计。但真正梳理业务需求后我发现“客户关系管理”这四个字背后的复杂性被严重低估了。如果你只是做一场活动把收集来的线索手动录入Excel那确实不需要CRM。但企业一旦进入销售阶段就会面临几个实际痛点销售手里攒了几百个客户哪个该今天跟进哪个报价发出去一周了没回音合同签了但回款还没到每个销售都很忙但管理者根本看不到过程数据只能听汇报。这时候CRM的价值就不是“存储客户信息”而是把销售过程拆成可量化、可追踪的阶段让管理层能看到“线索从哪来、卡在哪一步、为什么丢单”。所以拿到项目后我做的第一件事不是打开IDE而是先把业务边界圈出来。这个基于SpringBootVue的CRM系统核心模块可以分为五块系统管理用户、角色、菜单、线索管理公海池、私海池、客户管理客户档案、联系人、商机管理销售阶段、成交概率、跟进与合同跟进记录、合同回款。后面所有的代码、表结构、权限设计都要围绕这五块来展开。1.2 为什么“源码直接部署”在大多数公司根本不成立网上确实能搜到大量CRM源码很多标着“开箱即用”。但真正部署起来你会发现首先数据库脚本常常是残缺的要么少字段要么默认数据带着一堆莫名其妙的演示数据其次权限模型跟你的组织架构对不上——你公司可能是“销售总监-销售经理-销售专员”三级而别人的模板是“总经理-主管-客服”两级最要命的是字段设计无法扩展客户想要加一个“客户来源渠道”下拉框你得去改前后端N个地方。所以我的原则是源码可以拿来当参考骨架但必须自己梳理清楚业务规则把通用模块登录、RBAC权限、基础CRUD和业务模块解耦。这个基于SpringBootVue的项目通用模块做得相对干净Controller也没堆成一坨几百行的类这一点是它的优点。但它默认的业务字段覆盖不了所有行业场景比如教育培训机构需要记录“试听时间”制造业需要记录“项目预算和报价明细”这些就得二次开发。2. 技术选型背后的取舍SpringBootVueMyBatisMySQL为什么这么配2.1 后端框架和ORM选型MyBatis比JPA更适合这种“多表关联动态条件”场景现在SpringBoot虽然是默认选项但ORM的选择很多人会纠结。Spring Data JPA写起来确实爽仓库接口里定义个方法名字就能查询但一旦涉及CRM这种动不动就联查五张表、过滤条件随前端表单动态拼接的业务JPA的Specification和Query写起来就非常痛苦而且调试SQL很不直观。MyBatis的优势在于动态SQL是它的主场。CRM里最常见的操作就是“根据客户名称、电话、负责人、创建时间、客户状态”等多个可空条件组合查询用MyBatis的if标签可以很直白地拼SQL看日志就知道查了什么。由于这套系统用的是原生MyBatis而不是MyBatis-Plus我一开始还担心有很多重复的CRUD代码结果发现作者在通用Mapper也就是内置的BaseMapper接口上做了抽取。这个设计很聪明单表操作用通用方法多表关联查询写在XML里既不浪费MyBatis的灵活性又保住了开发效率。2.2 前端为什么选Vue而不是React不是React不好而是Vue的上手成本和中文生态更适合这种面向中小团队的管理系统。CRM项目有大量表格、表单、弹窗、抽屉这类中后台组件Vue的v-model双向绑定和.sync修饰符让表单状态管理特别顺手。这套项目用的是Vue2 Element UI的组合虽然2025年看起来不算新但胜在稳定Element UI的组件覆盖了95%的后台管理页面需求几乎不需要自己造轮子。有人会问不用Vue3element-plus吗如果是从零新写我肯定推荐Vue3的组合式API语法更简洁响应式性能也更好。但针对这个源码我更看重的是它的现有业务代码量——Vue2语法写习惯了改造起来反而容易引入问题。如果你后续打算升级Vue3我建议优先把路由权限这套核心逻辑重写而不是替换UI组件库。2.3 MySQL的版本与存储引擎选择MySQL在这里没什么悬念但有几个细节值得说。字符集一定要统一成utf8mb4不然后面存emoji或生僻字会直接报错。存储引擎用InnoDB因为它支持事务——CRM里的“领取线索”操作涉及到修改线索状态、记录操作日志、更新统计数量这三个动作必须在一个事务里完成缺一个都可能造成数据不一致。排序规则推荐utf8mb4_general_ci如果你要支持更复杂的多语言排序可以换utf8mb4_unicode_ci但大多数场景下前者已经够用且性能更好。另外MySQL 8.0和5.7的差异需要提前关注。SpringBoot的最新版本默认驱动是com.mysql.cj.jdbc.Driver如果你的服务器还是5.7需要把驱动改成com.mysql.jdbc.Driver同时注意时区参数serverTimezoneAsia/Shanghai不然连数据库都会报错。这套项目在开发时用的MySQL 8.0部署文档里没有特意强调新手很容易在这里卡住。3. 数据库设计把“客户关系”翻译成五张核心表3.1 用户、角色、菜单三张表搭建RBAC权限底座系统管理模块的核心是权限权限的数据基础是三张表sys_user用户、sys_role角色、sys_menu菜单/权限项。用户和角色是多对多角色和菜单也是多对多所以还需要两张中间表sys_user_role和sys_role_menu。很多刚入门的人会把权限直接挂在用户上那样一旦给十几个销售分别配置权限工作量会爆炸。用RBAC基于角色的权限控制之后只需要给角色配置一遍菜单权限再把用户塞进角色里全部搞定。sys_menu这张表很关键它不止存菜单名称和路径还存了菜单类型目录/菜单/按钮。按钮权限的要点就在这——比如“客户删除”这个按钮其实是作为一条按钮类型的菜单记录存在角色关联了这个按钮才看得到。前端拿到按钮权限标识后通过自定义指令控制v-if后端的接口再校验一遍形成双重防线。这套源码在这一块做得比较规范值得学习。3.2 线索、客户、商机、合同的转化链路业务核心的表围绕一条主线展开线索lead→ 客户customer→ 商机business→ 合同contract。线索像是一个“潜在客户”可能来自展会、网络推广、朋友介绍信息不完整。当线索经过电话沟通并被确认有效后可以转化为客户客户进一步沟通产生购买意向就可以创建商机商机被推到某个销售阶段最后报价成功就签合同合同关联回款记录。这套设计最舒服的点是把“过程数据”和“结果数据”分开。销售员每天跟进的是线索和商机而不是直接改合同管理层看报表的时候可以通过商机阶段分布知道销售漏斗是否健康。customer表里通常会有level客户等级、source来源渠道、industry所属行业等字段这些在后面统计报表里都是重要的筛选维度。有一个容易忽略的细节客户负责人字段owner_id必须建立索引。因为CRM里最频繁的查询就是“我负责的客户列表”如果没有索引数据量到几万条后分页查询就会变慢。同样的线索表和商机表上的owner_id和status字段也建议加上组合索引。3.3 跟进记录和自定义字段最容易被低估的设计很多CRM项目把跟进记录设计成一张大宽表一个字段专门存文本一个字段存下次跟进时间看着简单但实际上当销售需要记录“客户说预算有20万但是付款周期要三个月”这类结构化信息时宽表就很无力。这个项目的做法是follow_up_record表关联业务类型和业务ID具体内容用JSON字符串存储。这种设计牺牲了一点查询性能但换来了极大的灵活性。在二次开发时如果你要给“线索”新增一个“预计成交金额”字段不需要改表结构直接在前端表单加一个录入项存进JSON字段里就行。当然如果你需要拿这个字段做统计筛选那还是要拆出来建独立列。我的建议是短期的、展示型的字段用JSON长期要参与报表分析的字段用独立列不要一刀切。4. 后端核心实现从JWT认证到两层权限拦截4.1 无状态认证怎么做才不出安全事故登录模块用的是JWTJSON Web Token。流程是用户输入账号密码后端校验通过后生成一个tokentoken里携带用户ID和登录名的声明返回给前端。前端把token存到LocalStorage或SessionStorage里每次请求在Header里带上Authorization: Bearer token后端通过拦截器解析token再放行。这里最容易踩的坑是“token过期和注销问题”。一般JWT一旦签发在过期之前服务端是没办法让这个token作废的。如果你在系统里做了强制退出的功能直接删除前端缓存是不够的——别人拿到旧token还是能正常调接口。这套源码的处理方式是维护一个token黑名单列表用户注销时把token加入黑名单拦截器里先查黑名单再校验有效期。这个做法牺牲了一点存储空间但避免了JWT状态下无法主动失效的尴尬在管理后台这种低并发的场景里非常合适。权限校验不能只靠前端藏按钮后端必须再验一遍。我见过太多项目Controller接口上只写RequestMapping没有任何权限注解导致用户直接拿着URL就能访问别人不该看的接口。这个项目在PreAuthorize注解上做了封装配合Spring Security每个业务接口都标注了需要的权限标识符。如果不想引入Spring Security用拦截器也能实现但Security的注解式权限校验更清晰团队接手起来也更规范。4.2 MyBatis动态SQL实现复杂列表查询CRM列表页的查询条件非常多我举一个实际例子客户列表页要支持按客户名称模糊匹配、按行业选择、按负责人选择、按创建时间段筛选、按客户状态筛选同时还要排除掉那些已经被当前用户“私有化”的客户。如果把这些条件全部写在业务代码里用if判断去拼装查询条件代码会非常难维护。MyBatis XML的动态SQL就是干这个的。在Mapper对应的XML文件里使用where标签结合if testcustName ! null and custName ! 这样的条件可以让SQL语句自动组装。更高级的用法是使用foreach处理集合参数比如批量分配客户时传入客户ID数组customerIds生成AND id IN (...)。这里有几个容易踩的坑第一if判断空字符串时要注意数据库字段为null和空串不是一回事第二模糊查询LIKE CONCAT(%, #{custName}, %)一定不要写成%${custName}%用${}会把传参直接拼进SQL百分百的SQL注入漏洞。4.3 数据权限控制不能让销售看到别人的客户如果说功能权限是横切面那么数据权限就是纵切面。租户级的CRM里普通销售只能看到自己负责的客户销售经理能看到自己团队所有成员的客户老板才能看到全部客户。这个逻辑怎么实现常见的做法是在SQL层自动追加数据范围条件而不是靠业务代码去判断。具体来说可以在MyBatis的拦截器里解析当前登录用户的角色根据角色对应的数据权限范围本人、本部门、全部自动拼接过滤条件。但拦截器逻辑复杂容易误伤其他查询源码里的做法是更朴素的在Mapper接口的查询方法上传入一个DataScope参数XML里用if testdataScope.scope ! 0来动态拼接。这个方案牺牲了一点通用性但胜在直观、可控新手也能看懂。5. 前端Vue实现动态路由和按钮权限才是真正的门面5.1 动态路由菜单不能写死在代码里第一眼看到这个前端工程会觉得它和普通Vue后台项目没啥两样vue-router定义路由Element UI布局Axios发请求。但仔细看路由配置你会发现router/index.js里并没有把所有页面都写死而是只保留了登录页、404、Layout框架这些固定路由。其余的业务页面比如客户管理、线索管理、商机管理的路由是在用户登录成功后根据菜单权限接口返回的路由列表动态添加到路由器中的。这样做的好处不言而喻如果产品经理下周改了菜单名称或者给销售角色新增了一个“商机看板”页面后端改一下菜单表和权限绑定前端不需要发版用户刷新页面就能看到新菜单。动态路由的实现要点有两个一是前端要有一个generateRoutes方法把后端返回的菜单树转换成Vue Router的RouteRecordRaw数组二是每次路由变化时要同步更新侧边栏菜单的展开和选中状态否则页签和菜单高亮对不上。5.2 Axios统一封装不只是省代码多数项目都会封装Axios但这个CRM的封装有几个值得借鉴的细节。第一请求拦截器里统一从Storage取出token附到请求头同时要做token过期判断如果发现HTTP响应是401直接跳转登录页并清空用户信息。第二响应拦截器里统一处理后端返回的业务错误码例如code500时弹出Error提示code403时提示无权限不会让每个页面都去写重复的try-catch。还有一个容易被忽略的点请求取消。用户在客户列表页快速翻页时上一页的请求还没返回就被当前页面覆盖经常会产生竞态问题。这个项目的做法是在每次路由切换时调用cancelAllRequest()把还没完成的请求通过AbortController或Axios的CancelToken取消掉。这个小细节能明显减少因接口响应乱序导致的数据错乱很多管理后台项目都没注意到。5.3 通用组件的抽法让客户表单和线索表单复用一套底子如果你去翻这个项目的components目录会发现不仅有分页组件、搜索表单组件还有一套基于配置的表单渲染方案。所谓基于配置就是定义一个对象数组每个对象描述表单项的类型、字段名、标题、校验规则、是否必填。前端拿到这个数组循环渲染就能动态生成表单页面。这样写的好处是客户管理和线索管理的表单结构大同小异几乎可以复用一套驱动代码。但我不建议新手一上来就搞这种组件化因为过度抽象会让调试变难。更实际的做法是先按普通方式写两个页面找到重复部分再抽象。比如表单中“客户名称”的输入校验规则、“手机号码”的格式校验、下拉框数据的加载方式这些逻辑抽象成公共组件才有意义如果一上来就抽几十个组件最后会发现每个组件都在传递不同的props反而比直接写还麻烦。6. 开发与部署中的踩坑实录那些日志不会直说的细节6.1 跨域配置前端端口8081后端端口8080到底谁来放行前后端分离项目最常见的第一个坑就是跨域。开发环境里Vue通常是localhost:8081SpringBoot是localhost:8080浏览器默认禁止跨域请求。这个项目的解决办法是在后端写一个CorsConfig用WebMvcConfigurer注册跨域规则允许所有来源、所有请求头和所有方法。这种全放开的方式开发方便但上线前必须收紧。安全建议是跨域配置里把allowedOriginPatterns设置为具体的前端域名不要用*同时开启allowCredentials(true)时再用*会直接报错。如果用了Nginx做反向代理更推荐让Nginx把/api路径转发到后端前端只请求同域这样从根上就不存在跨域问题。6.2 时间时区为什么列表里的日期总是少8小时这个坑几乎每个SpringBoot项目都会遇到。当你用FastJSON或Jackson处理日期字段时如果后端实体类用的是LocalDateTime而MySQL连接串上没有加serverTimezoneGMT%2B8从数据库查出来的时间会被当成UTC时间再被转成北京时间的JSON字符串最终前端看到的创建时间就多了或少了8个小时。解决办法有两个一是在数据库连接URL中明确指定时区二是在SpringBoot的application.yml里配置spring.jackson.time-zone: GMT8。要注意的是如果用了MyBatis的typeHandler处理时间类型还要确保JDBC驱动的版本兼容LocalDateTime。如果你在开发环境正常、测试环境却差了8小时多半是两台服务器的时区设置不同别急着改代码先看服务器的date命令输出。6.3 慢查询和N1问题列表页转圈圈的元凶CRM这类系统UI上常见的问题就是列表页加载慢。一个隐形的性能杀手是“N1查询”先查了客户列表再逐条查出每个客户的负责人姓名、商机数量、最后跟进时间。如果客户有100条就会额外执行300条SQL页面上转圈圈自然就来了。要排查N1最简单的办法是在MyBatis配置里打开慢SQL日志把log-impl: org.apache.ibatis.logging.stdout.StdOutImpl打开看每条SQL的执行时间和数量。优化方法是使用关联查询或者提前在客户主查询里通过子查询/Join连出需要的统计字段避免循环里再查数据库。例如客户列表显示“最后跟进时间”可以用LEFT JOIN (SELECT customer_id, MAX(follow_time) AS last_time FROM follow_up_record GROUP BY customer_id) f ON f.customer_id c.id把结果一次性算出。还有一个细节分页参数如果不合法会导致全表扫描。前端传pageSize999999时如果后端没有限制最大分页大小数据库会被拖垮。建议在分页查询工具类里加上阈值校验比如pageSize最大不允许超过500超过就按500算。7. 拿到源码后如何快速跑起来并做二次开发7.1 本地部署的几个关键步骤这套系统的启动过程我帮你捋顺。第一步准备基础环境JDK 1.8及以上Maven 3.6Node.js 14MySQL 8.0。第二步执行根目录下的sql/init_db.sql初始化数据库它会建库、建表并插入默认管理员账号。第三步修改后端application-druid.yml里的数据库连接信息把username/password改成你本地的。第四步启动后端SpringBootCrmApplication等端口8080起来。第五步进入ruoyi-ui或者前端目录具体名字看工程结构执行npm install安装依赖然后npm run dev启动开发环境。需要注意的是前端默认的接口地址是http://localhost:8080如果你改了后端端口记得同步修改前端的vue.config.js里的代理配置否则所有请求都会404。如果你的Node版本太高比如Node 18以上运行旧版Webpack可能会报OpenSSLError这时候需要设置NODE_OPTIONS--openssl-legacy-provider或者在package.json的dev脚本里加上这个环境变量。7.2 二次开发优先级先动权限再动业务字段最后动报表一旦源码跑通别急着乱改页面。我个人的修改顺序是先理解并修改权限配置把默认的超级管理员角色和实际组织架构对齐务必先验证“新建一个销售角色、赋予部分菜单权限、用一个新账号登录、看侧边栏是否正确显示”这个闭环。这个动作做完说明你已经跑通了系统最核心的权限链路。第二步再加业务字段。比如要给客户表加一个“客户类型”字段流程是数据库加列→实体类加属性→Mapper XML的ResultMap加映射→前端表单加字段→列表列加显示。五处改动缺一不可漏掉任意一环都会在页面上看不到或者报错。第三步才碰报表和看板因为报表涉及聚合查询和日期统计逻辑相对复杂建议放在你对业务数据结构足够熟悉以后再做。项目跑稳之后说几句掏心窝的话我在部署这个系统的时候一开始也觉得“不就是个管理系统吗”真正调通了才发现细节都在数据权限和状态流转里。比如把线索转客户时原负责人、创建时间、转化来源必须正确复制跟进记录不能丢比如商机的阶段变了要自动给负责人生成一条待办提醒。这些业务逻辑不复杂但漏掉任意一环销售用起来就会觉得“系统管不住事”。所以如果你打算拿这套代码直接上生产我建议先把客户和线索的导入导出功能做好——一批老销售手里的Excel数据进不到系统里项目大概率会推不动。然后是移动端适配现在的销售天天在外面跑只支持PC端的CRM很难真正用起来。我自己已经计划把后端接口按/api/v2的形式升级前端逐步迁移到Vue3和Element Plus同时把跟进记录的JSON字段抽成独立的业务表方便后续做更好的销售漏斗分析。如果你最近也在折腾SpringBootVue的客户关系管理系统建议不要只盯着“源码可用”这件事多花点时间把“数据流转”和“权限控制”这两个核心筋脉打通。真到了上线那天你会发现最有价值的反而不是那些页面而是你对业务规则的理解。希望这篇实操拆解能帮你少走点弯路。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。