十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot+Vue全栈CRM源码解析:从四层架构到前后端实践

Spring Boot+Vue全栈CRM源码解析:从四层架构到前后端实践 简介这是一套面向企业销售人员与全栈开发者的CRM系统源码基于Spring Boot和Vue前后端分离架构涵盖销售线索管理、商机管理、客户信息管理以及销售业绩跟踪等核心业务能够帮助企业有效降低客户信息分散和跟进不及时的问题提升转化效率。压缩包共733个文件大小约4.19MB以296个Java文件和129个Vue文件为主其余包括JS脚本、SVG图标、XML配置、SCSS样式以及构建/启动脚本等整体结构完整可直接作为前后端分离项目的工程模板。目前已有60人学习/下载适合用来学习企业级CRM开发或进行二次扩展。资源不仅展示了从后端接口设计到前端页面交互的完整实现还提供了本地运行所需的环境配置和快捷脚本便于读者快速启动项目、阅读关键模块代码并理解Spring Boot与Vue的协作方式是一份实用且有参考价值的全栈实操源码。1. 汇客CRM源码背后的技术骨架Spring Boot与Vue的组合逻辑很多团队在选型客户管理系统时习惯先去对比各种SaaS产品但稍微一算账就会发现问题按席位收费、数据在别人手里、定制一个字段要排半个月期。于是“买源码自己改”成了不少中小公司、软件外包团队和独立开发者的实际选项。一个名为“汇客CRM系统”的源码包在技术栈上选择Spring Boot和Vue本质上是走了一条当前Java后端与前端工程化最成熟的路线后端用Spring Boot快速提供REST接口前端用Vue组件化搭建业务界面两者通过JSON交互。这套源码的价值不在于“CRM”这三个字母——客户管理、线索跟进、合同回款这些业务概念在任何语言里都能实现——而在于它把Spring Boot的四层架构、MyBatis持久层处理、Vue的路由与状态管理、以及前后端联调时的接口约定完整地串在了一个真实业务场景里。如果你正打算接触这类全栈项目或者需要在一个已有系统基础上做二次开发先看懂这个骨架比急着跑起来更重要。下面我会按“数据模型→后端接口→前端页面→联调部署”的顺序把这个源码包应有的结构和你需要关注的技术点拆开讲。2. 从业务表到REST接口汇客CRM的四层架构与数据模型设计2.1 先明白“四层架构”在CRM项目里到底怎么切热词里经常出现“spring boot四层架构”很多初学者以为这是Spring Boot框架自带的规定。其实四层架构是业务项目的分层习惯Controller接口层→ Service业务层→ Mapper数据访问层→ Entity实体层。在汇客CRM这种规模的系统里这个分层直接决定了你拿到源码后能不能快速定位某个功能。Controller层只做参数接收和结果封装。它不写SQL也不写if-else业务判断。比如新建一个客户Controller拿到前端传来的JSON调用Service的saveCustomer方法然后把统一返回体Result对象丢给前端。Service层负责真正的业务规则手机号唯一性校验、客户是否已存在、操作权限判断。Mapper层在MyBatis-Plus场景下通常只需要继承BaseMapper接口复杂查询才自己写XML。Entity层对应数据库表字段命名建议保持下划线风格通过TableField注解映射到驼峰属性。拿到源码后第一件事不是直接启动而是先看entity目录下有几个实体类。通常一个CRM系统至少有这些客户、线索、跟进记录、合同、用户、角色、菜单。每个实体类对应一张核心业务表。把实体和表对应关系理清楚你对这个系统的理解就完成了30%。2.2 核心表结构客户、线索、跟进记录的关系汇客CRM属于“以客户为中心”的传统CRM模型数据结构围绕客户这条主线展开。线索表crm_clue用来存放从市场活动、地推、官网留资等渠道进来的原始信息字段一般包括姓名、电话、来源渠道、意向等级。客户表crm_customer则存放已经确认过需求、进入正式跟进阶段的公司或个人。线索可以转换为客户转换后线索表的状态字段变为“已转换”同时生成一条客户记录。跟进记录表crm_follow_record是CRM里最容易忽略但实际使用频率最高的表。它记录每一次销售与客户的交互内容字段至少要包括客户ID、跟进方式、跟进内容、下次跟进时间、跟进人。业务上有个硬性规则每次跟进后必须填写记录否则销售漏斗数据不完整。数据库层面通常用外键逻辑关联但为了查询效率大多数源码不会建物理外键而是靠Service层保证引用完整性。合同表crm_contract关联客户ID和产品信息字段包括合同金额、签约日期、开始日期、结束日期、状态。后续的统计看板——比如本月签约金额、回款计划——都是从这张表聚合出来的。表与表之间常见的关系是一个客户有多条跟进记录一条客户可以对应多份合同一个线索只能转换成一个客户。CREATE TABLE crm_customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_name VARCHAR(100) NOT NULL COMMENT 客户名称, phone VARCHAR(20) COMMENT 联系电话, source VARCHAR(50) COMMENT 来源渠道, level VARCHAR(20) COMMENT 意向等级, owner_id BIGINT COMMENT 负责人ID, status INT DEFAULT 0 COMMENT 状态0跟进中 1已成交 2已流失, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表的关键字段是owner_id和status。owner_id标识这条客户数据属于哪个销售这是CRM数据权限的基础。status字段则用于列表页的筛选比如只查“跟进中”的客户。注意这里的枚举值最好用整数或字符串常量不要直接存中文否则后续做统计聚合时处理起来很麻烦。2.3 用MyBatis-Plus快速实现CRUD的代码范式Spring Boot集成MyBatis-Plus几乎是当前CRM系统源码的标准配置。它和原生MyBatis的区别是单表CRUD不需要写SQL框架自动生成。你只需要定义好实体类和Mapper接口。public interface CrmCustomerMapper extends BaseMapperCrmCustomer { // 复杂查询可以在这里定义方法XML里写SQL PageCrmCustomerVO selectCustomerPage(Page? page, Param(query) CustomerQuery query); }Service层注入这个Mapper后新增操作一行调用。MyBatis-Plus的优势在于分页查询用Page对象作为第一参数框架自动拼接LIMIT语句返回结果自带总记录数。这是手写SQL时需要额外处理的能省不少代码。参数说明BaseMapper提供了insert、deleteById、selectById、updateById、selectPage这五个核心方法覆盖了实际开发中80%的单表操作。剩下的复杂查询——比如客户列表需要关联跟进次数、合同总金额——才需要写XML。这里有个常见的坑很多人误以为MyBatis-Plus不能写SQL。其实它支持两种方式并存复杂SQL放在resources/mapper目录下的XML文件里通过namespace绑定Mapper接口。所以我建议你在源码里找一下mapper目录的XML文件看它们的SQL写法是否符合规范这是判断这套源码质量的重要依据。3. Spring Boot端落地接口规范、权限体系与安全配置3.1 统一返回体与全局异常处理前后端分离项目中最核心的接口约定是返回结构。汇客CRM这类源码通常会定义一个Result类把所有接口的返回值包成一个固定形状code、message、data。前端收到响应后先判断code是否为200再取出data渲染页面。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }配合这个类的还有一个全局异常处理器用RestControllerAdvice注解标注拦截所有Controller层抛出的异常。比如参数校验失败抛出BizException处理器捕获后返回error结果前端就不需要针对每种异常写try-catch。这里注意一个细节异常信息不要直接返回给前端而是通过code区分错误类别message显示通用提示语。否则会把内部SQL错误曝露给浏览器存在安全隐患。这个返回体设计带来的直接好处是前端可以统一处理接口。后面讲Vue请求封装时你只需要写一个响应拦截器判断code值即可。3.2 RBAC权限模型与JWT登录流程CRM系统的权限控制和普通博客系统不一样它需要同时满足“功能权限”和“数据权限”两重需求。所谓功能权限就是某个用户能不能看到“合同管理”这个菜单所谓数据权限就是同一个菜单下普通销售只能看自己的客户销售经理能看整个团队的。汇客CRM的源码里一般用RBAC模型用户-角色-菜单实现。用户表存账号密码和所属部门角色表存角色名称和标识菜单表存前端路由和按钮标识。用户和角色是多对多关系角色和菜单是多对多关系。登录验证时后端根据用户ID查出角色和菜单生成一个JWT Token返回给前端。Token里通常只包含用户ID和过期时间不包含权限数据——权限每次请求时从数据库查询避免修改权限后Token已过期导致的新权限不及时生效问题。PostMapping(/login) public ResultString login(RequestBody LoginDTO loginDTO) { // 1. 校验验证码若系统启用 // 2. 根据用户名查询用户比对加密后的密码 User user userService.getByUsername(loginDTO.getUsername()); if (user null || !encoder.matches(loginDTO.getPassword(), user.getPassword())) { throw new BizException(用户名或密码错误); } // 3. 生成JWT令牌 String token JwtUtil.createToken(user.getId(), user.getUsername()); return Result.success(token); }参数说明JwtUtil是工具类负责生成和解析Token。源码里通常有三个关键配置密钥secret、过期时间expireTime、请求头名称header默认Authorization。实际项目中密钥必须放到配置文件的加密环境变量里不能出现明文。Token生成后前端每次请求在请求头中携带这个Token后端通过Spring Security或拦截器校验。3.3 内部接口安全Actuator端点的暴露控制热词里出现“spring boot actuator未授权访问”“spring boot actuator 漏洞”这在CRM源码里同样值得确认。Actuator是Spring Boot提供的运维监控组件默认情况下会暴露一堆内部接口health、info、metrics、env、beans、mappings。如果env和beans接口未授权访问攻击者可能看到系统环境变量和Bean配置信息这就是通常说的“未授权访问”问题。但这个风险在CRM项目里并不难防——两种做法一是依赖Spring Security统一拦截Actuator路径做认证二是显式关闭不必要的端点。management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: never beans: enabled: false env: enabled: false这段配置的作用只开放health、info、metrics三个端点并把health的详情关掉。beans和env直接禁用。如果你的项目不需要外部探活甚至可以把include换成exclude全部关闭。这是一个成本极低但收益很高的安全配置项。拿到源码后打开application.yml搜management这个关键词看它的暴露范围是否合理。顺便说一句如果源码里已经有Spring Security更规范的做法是在SecurityConfig里加一行对/actuator/**路径的permitAll或hasRole控制——看这个接口是给运维用的还是给监控平台用的。按需放开而不是图省事全部拒绝。4. Vue端落地路由权限、请求拦截与客户看板实现4.1 前端环境与项目结构Vue项目拿到手之后先不要急着npm install先看package.json里的依赖版本。汇客CRM这类源码时间跨度很大有Vue 2 Element UI的老版本也有Vue 3 Vite Element Plus的新版本。区分方法很简单package.json里出现了vue: ^3.x那就是Vue 3项目出现vue-router: ^4.x则代表路由版本对应Vue 3。# 安装依赖 npm install # 本地开发启动 npm run devnpm install如果报错常见三种情况Node版本太高或太低导致node-sass编译失败Vue 2项目常见、registry源访问慢、lock文件冲突。我的处理顺序是先看Node版本Vue 3项目建议Node 16Vue 2项目Node 14更稳然后把npm源切到国内镜像最后删除node_modules和package-lock.json重新安装。不要一上来就改代码大部分前端启动问题都出在环境匹配。启动后项目的核心目录在src下api目录放接口请求router目录放路由配置store目录放Pinia或Vuex状态views目录放页面组件。拿到源码后先确认几个关键文件存在这决定了系统是否完整。4.2 动态路由与登录状态守卫CRM系统通常不是所有页面都对所有用户开放所以路由要做成动态的用户登录后后端返回该用户有权限的菜单列表前端把菜单渲染到侧边栏同时动态注册对应的路由组件。实现逻辑上是把静态路由表拆成两份一份是基础路由登录页、404页一份是业务路由所有页面组件的映射表。// router/index.js const routes [ { path: /login, component: Login }, { path: /, component: Layout, children: [] } ]; // 动态注册业务路由 function addDynamicRoutes(menus) { menus.forEach(menu { const route { path: menu.path, name: menu.name, component: () import(../views/${menu.component}.vue) }; router.addRoute(Layout, route); }); }这个写法有两个关键点第一组件路径是字符串拼接出来的要求后端菜单表里的component字段必须和前端views目录下的文件路径严格对应比如客户管理对应customer/index.vue。第二addRoute必须在路由守卫里完成且不能重复注册——需要一个Set或Map记录已注册的路由name。路由守卫的作用是拦截未登录用户。当用户访问一个需要认证的页面但本地没有Token时就重定向到/login页。router.beforeEach((to, from, next) { const token localStorage.getItem(crm_token); if (to.path /login) { next(); } else if (!token) { next(/login); } else { next(); } });这段逻辑说不上复杂但它是所有前端权限体系的入口。注意这里只是做了登录态判断真正细粒度的按钮级权限比如“删除客户”按钮是否显示需要结合自定义指令v-permission实现判断当前用户角色是否拥有对应的按钮标识。4.3 请求封装与Token注入前端请求后端接口不能每写一个接口就复制一遍Axios配置而是封装成一个统一的request模块。import axios from axios; import { ElMessage } from element-plus; const service axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器注入Token service.interceptors.request.use(config { const token localStorage.getItem(crm_token); if (token) { config.headers[Authorization] Bearer token; } return config; }); // 响应拦截器统一处理code service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { // Token过期清除本地登录态跳转登录页 localStorage.removeItem(crm_token); window.location.href /login; } ElMessage.error(网络异常); return Promise.reject(error); } ); export default service;参数说明baseURL设为/api同时需要在vite.config.js里配置开发代理把/api开头的请求转发到后端地址这样既解决了跨域问题又能在生产环境通过Nginx统一转发。响应拦截器的401处理是重点当后端返回HTTP 401状态码时说明Token过期或非法此时应清除本地存储并强制跳转登录页而不是弹一个让人困惑的错误提示。4.4 客户看板ECharts数据可视化CRM系统通常会有一个首页看板展示本月的线索数量、新增客户、成交金额、跟进次数等统计指标。Vue端最常用的方案是ECharts。实现上后端提供一个聚合统计接口前端拿到数据后用ECharts渲染成图表。看板页面的核心并不是图表库怎么用而是如何从后端接口拿到正确的数据。我见过很多项目的看板慢不是因为图表渲染慢而是后端SQL没有聚合——一次性把明细数据传到前端再手动计算几万条数据就卡死了。正确的做法是后端把聚合逻辑放进SQL按日期分组计算数量按状态GROUP BY统计前端只负责展示结果。// 按月统计线索量 const result await getTrendData({ year: 2025 }); // result.data [{ month: 2025-01, count: 32 }, ...] myChart.setOption({ xAxis: { data: result.map(item item.month) }, series: [{ type: line, data: result.map(item item.count) }] });这里注意一个性能优化ECharts实例在组件销毁时一定要调用dispose方法否则页面频繁切换会导致内存泄漏、浏览器卡顿。在Vue 3的组合式API里这一步放在onUnmounted钩子里。图表的数据格式约定也值得说后端传过来的字段名month、count要统一。如果有的接口叫月份有的叫date前端就要在多个地方做映射维护成本很高。5. 从源码到可运行环境准备、配置文件与二次开发的三步走5.1 本地启动顺序不管源码包里有没有README本地的启动顺序都应该是先起数据库再起后端最后起前端。数据库的初始化通常依赖源码目录下的sql/mysql.sql脚本。直接在MySQL命令行执行即可不需要手动建库建表。# 1. 初始化数据库 mysql -uroot -p sql/mysql.sql # 2. 修改后端配置中的数据库连接 # 编辑 src/main/resources/application.yml # 3. 启动后端 mvn spring-boot:run # 4. 启动前端 cd web npm install npm run dev后端启动失败的排查路径是固定的先看端口是否被占用默认8080、再看数据库连接串是否能通、最后看Redis是否启动——如果这个系统用了Redis做缓存Redis连不上后端也会启动失败。还有一个高频坑是JDK版本不匹配Spring Boot版本和JDK版本必须对应Boot 2.x用JDK 8或11Boot 3.x必须用JDK 17。前端启动成功后会输出一个本地地址默认是http://localhost:5173。如果这时代理配置正确登录页面就能正常调通后端接口。5.2 一份完整的application.yml要点server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/huike_crm?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true jwt: secret: your-secret-key-change-me expire: 604800 # 7天参数说明数据库连接串里必须带serverTimezoneAsia/Shanghai否则MySQL 8的时区问题会导致时间字段错乱。map-underscore-to-camel-case是MyBatis-Plus的驼峰映射开关开启后数据库的create_time能自动映射到实体的createTime字段。jwt的secret是核心安全配置源码里如果写死了一个明文密钥部署前一定要改掉。5.3 二次开发的功能扩展路径如果你打算在这个源码基础上做私有化定制我的建议是遵循“先会员卡后订单”的改造逻辑——先做小范围、低风险的改动验证整个流程跑通后再做大功能。最典型的小改动是新增加一个客户字段比如“客户来源行业”。步骤拆下来就是三板斧第一步改数据库表加一个industry字段。第二步改实体类加对应的属性。第三步改前端表单页加一个输入框。这个流程走完后你会发现修改是双向的——后端不需要改接口因为新增表单提交时后端通过实体类接收未知字段会自动忽略真正的改动只在前端。但如果要增加一个完整的“报价单”模块那是另一个故事需要从建表、写Mapper、写Service、写Controller、写前端页面一路走下来。这个过程中注意保持四层架构的代码风格与源码一致不要自己另起炉灶。// 推荐新增查询接口时保持分页参数风格一致 public PageCrmCustomerVO queryCustomerPage(CustomerQuery query) { PageCrmCustomer page new Page(query.getPageNum(), query.getPageSize()); // 条件构造器 LambdaQueryWrapperCrmCustomer wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.isNotBlank(query.getLevel()), CrmCustomer::getLevel, query.getLevel()) .like(StringUtils.isNotBlank(query.getCustomerName()), CrmCustomer::getCustomerName, query.getCustomerName()) .orderByDesc(CrmCustomer::getCreateTime); return customerMapper.selectPage(page, wrapper); }部署环节我推荐直接用Docker Compose把MySQL和Redis做成容器后端打成jar包用systemd管理前端打包后丢到Nginx的static目录。前端打包前要改一个关键配置vite.config.js里的代理路径只有在开发环境生效生产环境需要靠Nginx反向代理把/api转发到后端。这个配置漏掉就会出现一个经典问题打包后登录页开了但登录请求全部404。到时候检查的顺序是先看Nginx的location规则再确认后端接口响应最后看前端请求的URL路径拼接。本文还有配套的精品资源点击获取
返回列表