公司动态
Spring Boot + Vue.js 实现客户关系管理系统(CRM)实战全解析
简介一套基于Spring Boot与Vue.js的客户关系管理系统CRM设计实现项目。项目面向需要对前后端分离架构、企业级Web开发进行实战练习的开发者与计算机专业学生涵盖客户信息管理、销售机会跟踪、服务记录等典型业务场景解决从零搭建CRM系统时缺少完整可运行代码与设计文档的问题。压缩包共383个文件大小13.65MB核心内容包括93个Java后端源码、48个Vue前端组件、17个JS脚本、11个XML配置以及构建与启动用的bat脚本、YML配置、数据库SQL脚本、论文doc文档等另有大量SVG/PNG图标与字体文件用于界面展示目录组织结构清晰。包内论文.doc详细阐述系统设计与实现思路db.sql提供数据库表结构说明文档.txt含安装部署步骤可直接导入数据库并启动运行方便对照源代码理解Spring Boot API接口、Vue组件化开发与数据绑定机制。已有138人学习下载适合作为课程设计、毕业设计参考或企业CRM项目的入门蓝本。 Spring Boot 185 这个编号用过的人看一眼就懂——这是网上流传的毕业设计或课程设计源码包命名方式。标题里的“基于 vue.js 的客户关系管理系统CRM”本质上就是一套典型的前后端分离项目后端用 Spring Boot 提供接口前端用 Vue.js 渲染页面核心业务围绕“客户、联系人、跟进记录、商机”这几张表转。这篇文章我就拿这个项目当抓手把 CRM 系统的设计思路、技术选型、数据库建模、核心接口实现、以及最容易踩的坑完整过一遍。不管你是刚拿到这份 .rar 源码准备改项目还是想自己从零写一套 CRM 练手这篇都适用。我会尽量说清楚“为什么这么做”而不是只给你一堆能跑但不理解代码。1. 项目整体设计与技术选型解析1.1 这个 CRM 系统的核心功能边界客户关系管理系统这个名词听起来很大但毕业设计级别的 CRM 实质上只做三件核心事管客户、管跟进、管商机。管客户就是维护客户的基本档案包括公司名称、联系人、电话、地址、客户等级、所属行业等。管跟进是指每次销售和客户的电话沟通、微信聊天、上门拜访记录都需要沉淀下来形成跟进时间线。管商机是把有购买意向的客户转为商机记录预计成交金额、预计成交日期、当前阶段初步接触、需求确认、方案报价、谈判中、赢单/输单。这套系统在这个基础上还会附带几个辅助模块数据统计看板按月份统计新增客户量、成交金额、用户管理管理员可以创建销售人员账号、以及操作日志谁在什么时候改了什么数据。范围控制得很聪明不多不少刚好能完整体现一个业务闭环又不会因为功能膨胀导致写不完。1.2 为什么是 Spring Boot Vue.js 这套组合项目名里出现“springboot185”这种标号说明它来自某个教学资源站点技术上基本都是当年最主流的组合。虽然现在 Spring Boot 已经出到 3.x、JDK 也到 21 了但这套项目选择了更稳妥的Spring Boot 2.x Vue.js 2.x Element UI这个选型放在今天看依然是毕业设计最稳的搭配。原因有三点。第一Spring Boot 2.7.x 的生态资料极其丰富遇到问题搜索一下基本都能找到答案。相比之下Spring Boot 3.0 以后强制要求 JDK 17很多老教程的代码直接跑不起来只会增加你调试的负担。第二Vue.js 2.x Element UI 的组合是中文技术圈里资料最密集的前端搭配。Element UI 的表格、表单、弹窗、分页组件开箱即用不需要自己写复杂样式非常适合业务系统快速开发。第三前后端分离的开发模式是当前企业的主流形态。前端跑在 8080 端口通过 axios 请求后端的 8081 端口两边独立部署、独立开发。这种架构在写简历和答辩时也更好讲——你可以明确说出“我用了 RESTful API 设计风格接口和页面解耦”。提示如果你拿到的是 Spring Boot 2.x 的项目别急着升级到 3.x。JDK 切换、javax 到 jakarta 包名迁移、Redis 连接方式变更这些坑足够你折腾好几天而且对功能本身没有任何提升。1.3 项目模块划分和目录结构设计拿到一份 .rar 源码包第一步不是急着启动而是先把目录结构看懂。一个规范的 Spring Boot Vue.js 前后端分离项目应该长这样crm-system/ ├── backend/ # Spring Boot 后端 │ ├── src/main/java/com/example/crm/ │ │ ├── controller/ # 接口层接收前端请求 │ │ ├── service/ # 业务逻辑层处理核心业务 │ │ ├── mapper/ # MyBatis 数据访问层 │ │ ├── entity/ # 实体类映射数据库表 │ │ ├── config/ # 配置类跨域、拦截器、Swagger │ │ ├── common/ # 公共工具类、统一返回结果 │ │ └── CrmApplication.java # 启动类 │ └── src/main/resources/ │ ├── application.yml # 配置文件 │ └── mapper/ # MyBatis XML 文件 ├── frontend/ # Vue.js 前端 │ ├── src/ │ │ ├── views/ # 页面客户列表、跟进记录、商机看板等 │ │ ├── router/ # 路由配置 │ │ ├── api/ # 接口封装 │ │ ├── components/ # 公共组件 │ │ └── App.vue # 根组件 │ └── package.json └── sql/ # 数据库初始化脚本 └── crm.sql前后端目录必须分离清楚。我见过很多学生把 Vue 打包后的静态文件硬塞进 Spring Boot 的 static 目录里那叫假前后端分离答辩被追问几句就露馅。2. 核心模块设计与数据库建模2.1 关键数据表设计数据库是 CRM 系统的地基表设计直接决定了后续代码好不好写。这套系统的核心表我用下面这几张来说表名核心字段作用说明customerid, name, industry, level, phone, address, owner_id, create_time客户主表记录基本信息contactid, customer_id, name, position, phone, wechat联系人表一个客户对应多个联系人follow_recordid, customer_id, content, type, next_time, create_by, create_time跟进记录表记录销售沟通反馈businessid, customer_id, name, amount, stage, expected_time, status商机表记录潜在交易机会sys_userid, username, password, real_name, role_id用户表登录账号体系这里有个容易忽略但非常重要的设计细节customer 表里必须有一个 owner_id 字段。它的作用是标记“这个客户是哪个销售负责的”。有了它后端在做列表查询时只需要加一条where owner_id 当前登录用户id就能实现每个销售只能看到自己客户的权限隔离。数据库脚本里一般会预置一个管理员账号admin/admin123和一个普通销售账号。密码在 SQL 脚本里通常是明文但如果你要拿去答辩或者实际部署务必改成 BCrypt 加密存储。2.2 客户管理的状态流转设计如果只是简单地增删改查这个系统就没什么含金量了。稍微好一点的设计会在客户模块引入状态流转潜在客户 → 意向客户 → 成交客户 → 流失客户。这个流转可以用一个简单的status字段配合业务逻辑实现。比如销售把某个客户的状态改成“意向客户”后系统会提示他创建一条跟进记录说明这个客户为什么有购买意向。如果想严谨一点还可以加一张customer_status_log表记录每次状态变更的操作人和时间这样溯源时能看到完整的流转历史。我第一次做类似系统的时候就把状态字段设计成普通的 int 类型前端直接下拉框选择。后来被业务方问到“这个客户是谁在什么时候从哪个状态转过来的”当场答不上来。所以后续我改进的版本都会加一张状态变更日志表。虽然多写一个接口但这种“留痕”设计才是真实 CRM 系统的业务要求。2.3 级联删除还是逻辑删除客户和联系人、跟进记录之间是明确的一对多关系。那问题来了删除一个客户时他的联系人和跟进记录怎么办很多初学者会用外键 级联删除数据库层面ON DELETE CASCADE删客户时子表数据自动消失。看起来省事但这样做特别危险。真实业务中跟进记录往往包含销售的重要过程数据删掉了就再也找不回来。我的建议是所以涉及业务数据的表都做逻辑删除。就是在表里加一个deleted字段0 表示正常1 表示已删除所有查询默认带上where deleted 0。删除操作实际上是执行update customer set deleted 1 where id xxx不是真正的物理删除。注意逻辑删除的代价是每条 SQL 都要记得带过滤条件如果你用的是 MyBatis-Plus它有专门的TableLogic注解帮你自动处理省心很多。如果手写 MyBatis XML那就要注意公共 SQL 片段的抽取。3. 实操核心流程与关键代码实现3.1 后端的接口分层设计思想Spring Boot 后端最标准的开发方式是三层架构Controller 负责接收和返回请求Service 负责业务逻辑Mapper 负责数据库操作。这套 CRM 系统的核心接口路径应该按资源来设计而不是按操作来设计。比如GET /api/customer/list?page1size10keyword张三 查询客户分页列表 POST /api/customer 新增客户 PUT /api/customer/{id} 编辑客户 DELETE /api/customer/{id} 删除客户逻辑删除 GET /api/customer/{id}/follow 查询某客户的跟进记录 POST /api/follow 新增跟进记录 GET /api/business/statistics?year2025 商机金额统计这种接口风格在答辩时非常好讲因为它是标准的 RESTful 风格每个接口的意图一目了然。Service 层有一个容易犯的错把 Controller 里的参数直接透传给 Mapper跳过了业务校验。比如新增跟进记录时应该校验客户是否存在、跟进内容不能为空、下次跟进时间不能早于当前时间。这些判断应该写在 Service 层而不是让 Controller 做个空值判断就完事。3.2 登录认证与 JWT 的实际落地CRM 系统不允许未登录的人访问业务接口所以必须有登录认证。这套项目最常见的方案是 JWTJSON Web Token流程是这样的用户输入账号密码后后端校验通过生成一个 token 字符串返回给前端。前端把它存在 localStorage 里之后每次发请求都在 Header 里带Authorization: Bearer token。后端用一个拦截器在请求到达 Controller 之前校验 token合法就放行不合法就返回 401 状态码。核心代码如下Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; // 放行跨域预检请求 } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); } try { // 解析token失败会抛出异常 Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\token无效或已过期\}); return false; } } }有同学会问为什么用 JWT 不用 Session核心原因是前后端分离架构下前端可能部署在完全不同的域名Session 依赖 Cookie 传递跨域场景下处理很麻烦。JWT 把用户身份信息直接编码在 token 里后端拿到就能解析不需要在服务端存任何会话状态天然适合前后端分离。但 JWT 也有明显的坑token 一旦签发在过期之前是无法吊销的。如果用户点了退出登录你只能在前端把 token 删掉算是一种“假退出”。这个问题在毕业设计里不用深究但如果要做企业级系统通常要引入 Redis 做 token 黑名单或者干脆用 Redis 存储 session 取代 JWT。3.3 前端 Vue.js 的模块化页面实现前端的核心思路是基于 Vue Router 做页面跳转基于 Axios 做接口请求基于 Element UI 的组件快速搭建界面。客户列表页面是核心中的核心。数据加载的流程是页面加载时调用loadCustomerList()方法请求后端接口拿到分页数据渲染成表格。搜索功能其实很简单把搜索框的关键字绑定到this.queryParams.keyword点搜索时带着这个参数重新请求一次接口就行。Vue 的一个经典坑是“异步时序问题”。比如你在created()里先调用获取客户列表的方法再调用获取统计数据的方法两个接口是并行发出的响应时间不确定。如果后一个接口返回得早、前一个接口返回得晚页面显示的数据就可能是旧的。解决办法是用Promise.all或者 API 的finally回调确保所有数据都加载完成后再去操作 DOM。Element UI 的分页组件绑定方式如下el-pagination current-changehandlePageChange size-changehandleSizeChange :current-pagequeryParams.page :page-sizequeryParams.size :totaltotal layouttotal, prev, pager, next, sizes /el-pagination对应的handlePageChange方法里把当前页码更新到queryParams.page然后重新调用加载方法。分页是所有管理系统的前端核心需求吃透这一个组件其它页面基本都是复制这套模式。3.4 从零启动项目的环境准备拿到源码包后如果你希望项目跑起来必须准备这些环境工具清单我整理一下工具推荐版本说明JDK1.8 或 11必须与项目 pom.xml 中配置的 java.version 一致Maven3.6用 IDEA 自带版本即可MySQL5.7 或 8.0导入项目根目录的 crm.sql 脚本Node.js14.x 或 16.xVue 2 项目不要用 Node 18否则 npm install 可能报错IDEA2021安装 Lombok 插件启动后端的顺序是先在 MySQL 中执行crm.sql建库建表然后修改 application.yml 里的数据库用户名密码接着运行 CrmApplication 的 main 方法。启动前端是npm install然后npm run serve默认跑在 8080 端口。这里重点强调一下 Node 版本。Vue 2 项目依赖 node-sass 的话新版本 Node.js 编译时极大概率报错。遇到这种问题最直接的解决方案是卸载重装 Node 16或者把 package.json 里的 node-sass 换成 dart-sasssass 包。我处理过好几个同学的报错信息基本都是这个原因。4. 常见问题与排查技巧实录4.1 启动失败的经典原因对照表我在帮人调试这类 Spring Boot Vue 项目时遇到过大量重复的问题。这里整理一张高频故障速查表直接对照排查症状根本原因解决办法启动类报Failed to configure a DataSourceapplication.yml 中数据库连接没配好或 MySQL 没启动确认数据库名、用户名、密码正确检查 MySQL 服务状态前端请求接口返回 404后端 Controller 路径和前端 api 文件里路径不一致逐个对比请求的 URL 和 RequestMapping 注解值请求接口返回 401 或 403没有带 token或 token 过期F12 查看请求头是否有 Authorization检查拦截器放行规则前端页面白屏控制台报Cannot read properties of undefined返回的数据结构和模板绑定的字段不匹配先看接口返回的 JSON 结构再看页面里绑定的字段名npm install报node-sass编译异常Node 版本过高换用 Node 16或把 node-sass 替换为 sass跨域请求报blocked by CORS policy前端端口和后端端口不同未配置跨域在后端增加全局跨域配置类这张表里的前三个问题占了日常排错的 70%。4.2 关于 Vue Devtools 检测不到的奇怪现象很多人搜过这样一段报错“Vue.js is detected on this page. Devtools inspection is not available because its in production mode or explicitly disabled by the author”。这个提示的意思是你打开的这个页面确实用了 Vue但它是以生产模式运行的所以 Devtools 插件不允许检查内部结构。这其实不是 bug是 Vue 框架的正常机制。生产环境下调试工具被禁用是为了防止别人轻易窥探前端代码逻辑。如果你用的这套 CRM 项目的后端把前端页面打包放进了 static 目录打开页面时 Vue 可能处于生产模式Devtools 就会显示这段话。这不太影响使用只是调试时看不到组件树会稍微难受一点。解决办法是单独启动前端开发服务器用npm run serve的方式访问页面Vue 会以开发模式运行Devtools 就能正常使用了。4.3 Spring Boot 版本太高导致的问题“springboot版本太高”这个热词背后是很多人的血泪教训。现在新建 Spring Boot 项目默认选 3.x 版本但很多老代码基于 2.x 编写直接升级必然爆雷。我遇到过的典型报错包括javax.servlet不存在。Spring Boot 3.x 把 Java EE 迁移到了 Jakarta EE包名从javax.*变成了jakarta.*。如果代码引用了javax.servlet.Filter、javax.annotation.Resource编译直接失败。解决方法是全局替换成jakarta.*或者干脆不升级把版本降回 2.7.x。MyBatis 整合方式变化。Spring Boot 3.x 中mybatis-spring-boot-starter需要 3.0 以上版本且配置项也有调整。如果你不确定最简单粗暴的办法就是保持项目原有的 Spring Boot 版本不动。一个认真做好的项目它的技术栈是内部自洽的不需要为了追新版本而升级。4.4 事务失效和循环依赖排查这两个关键词在 Spring Boot 面试里高频出现实际项目中同样会遇到。事务失效最常见的场景是同类内方法自调用。比如你在 Controller 里调用 Service 的createCustomer()方法这个方法加了Transactional内部调用了另一个方法createFollowRecord(int customerId)。如果createFollowRecord也用Transactional且抛出异常createCustomer的事务是回滚不了的。原因很简单createCustomer是通过this.createFollowRecord()直接调用的没有经过 Spring 的代理对象事务注解根本没生效。解决办法有两个要么把createFollowRecord的逻辑合并到createCustomer里直接写要么在另一个 Service 类中注入这个 Service通过外部调用来触发代理。循环依赖是指 A 服务依赖 B 服务、B 服务又依赖 A 服务形成一个圆形依赖链。Spring Boot 2.6 之前默认允许循环依赖从 2.6 开始默认禁止启动时直接报错。如果你看到The dependencies of some of the beans in the application context form a cycle的报错要么在配置里允许循环依赖要么重构代码把 A 和 B 互相依赖的公共部分抽取到 C 里。提示使用 Spring Boot 2.7.x 的项目循环依赖问题默认开启会报警告。如果是毕业设计我不建议你花太多时间重构在 application.yml 里设置spring.main.allow-circular-references: true就能跑起来。但从学习的角度要清楚这只是一个临时方案真实项目中还是要避免循环依赖。5. 扩展思考如何把这个 CRUD 项目讲出亮点5.1 给项目加点可以让答辩加分的功能很多人担心普通版 CRM 太水答辩时没东西可讲。我的建议是不要新增多功能而是把一个功能做深。这里推荐三个方向。数据可视化方向。目前多数 CRM 的统计模块只有简单的列表展示你可以引入 ECharts把每个月新增客户数、商机金额分布、销售业绩排名做成图表。前后端只需要新增一个统计接口返回聚合数据前端用 ECharts 渲染出折线图、柱状图。这个提升低成本、高回报答辩时视觉冲击力强。导入导出方向。客户数据导入导出是很真实的业务需求也是企业级系统的标配功能。后端集成 EasyExcel前端加一个上传按钮和一个导出按钮代码量不大但演示效果特别好。操作日志方向。在后台记录用户的每次操作谁在什么时间创建了客户、修改了客户名称等在答辩时能展示你对业务敏感性的理解。5.2 代码规范与质量的一次整理一个好的源码包代码质量和项目规范是能被人一眼看出来的。我建议你重点检查这几处统一返回结果。后端接口是否都返回了统一的 Result 结构code、message、data而不是直接返回一个 Map 或裸的对象。接口异常时是否通过全局异常处理器统一捕获而不是到处 try-catch 后往外抛一个让人看不懂的 500。参数校验。前端表单做了校验但后端接口是否同样做了校验比如新增客户时客户名为空的情况下 是否会被直接存进数据库加上NotNull、NotBlank等注解写起来很快但能让项目完整度提升一个档次。注释与命名。实体类的字段命名是否规范且与数据库对应关键业务方法是否有注释这直接关系到阅读代码的人对你印象的好坏。5.3 部署上线需要留意的事如果要把项目部署到服务器上演示有两点需要注意。前端项目用npm run build打包生成 dist 目录然后把 dist 里的静态文件交给 Nginx 或者放进 Spring Boot 的 static 目录都可以。后端打成 jar 包用java -jar crm-1.0.0.jar启动。很多人第一次部署会遇到前端请求后端接口 404 的问题。根本原因是前端代码里写死了完整地址像http://localhost:8081/api/xxx。部署到服务器后需要把这个地址改成服务器的 IP 和端口或者配置 Nginx 反向代理把/api开头的请求转发到后端服务否则前端页面上的请求全都发到本机去了。另外还有一个容易踩的坑MySQL 数据库连接地址不要写成jdbc:mysql://localhost:3306/crm要写成公网 IP 或域名。本地跑通不代表服务器上也能跑通这个细节很多人到部署时才知道来回折腾半天。6. 个人实操心得与项目复盘手上过过好几个版本的 CRM 项目之后我个人最大的体会是这一类的管理系统表面上都是增删改查但设计思路上的差距非常大。你只有在动手实现“客户 跟进记录 商机”这条业务链路的过程中才能真正理解为什么会需要外键约束、为什么查询要分页、为什么要做权限隔离、为什么接口要统一返回结构。如果给你一份项目源码不要直接复制粘贴开始改。我建议你先带着三个问题去读代码每个接口干了什么事每个表字段为什么存在删除一个客户会发生什么把这三个问题弄明白这套系统基本上就属于你了。最后分享一个很实用的小技巧数据库的 sql 初始化脚本可以先手动执行一遍如果报错了不要慌看错误信息里提到哪个表、哪个字段。很多 SQL 脚本在流传过程中会被反复修改出现字段缺失、外键重复这类问题很正常。你把初始化脚本搞清楚就是先把系统的底摸清了。再多的接口代码、前端组件都是在这个底上一层一层搭起来的。本文还有配套的精品资源点击获取