简介:这是一套面向企业销售人员与全栈开发者的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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这张表的关键字段是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 BaseMapper<CrmCustomer> { // 复杂查询可以在这里定义方法,XML里写SQL Page<CrmCustomerVO> 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 Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> 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 Result<String> 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 16+,Vue 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或11,Boot 3.x必须用JDK 17。
前端启动成功后会输出一个本地地址,默认是http://localhost:5173。如果这时代理配置正确,登录页面就能正常调通后端接口。
5.2 一份完整的application.yml要点
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/huike_crm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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天参数说明:数据库连接串里必须带serverTimezone=Asia/Shanghai,否则MySQL 8的时区问题会导致时间字段错乱。map-underscore-to-camel-case是MyBatis-Plus的驼峰映射开关,开启后数据库的create_time能自动映射到实体的createTime字段。jwt的secret是核心安全配置,源码里如果写死了一个明文密钥,部署前一定要改掉。
5.3 二次开发的功能扩展路径
如果你打算在这个源码基础上做私有化定制,我的建议是遵循“先会员卡后订单”的改造逻辑——先做小范围、低风险的改动,验证整个流程跑通后再做大功能。最典型的小改动是新增加一个客户字段,比如“客户来源行业”。步骤拆下来就是三板斧:
第一步,改数据库表,加一个industry字段。第二步,改实体类,加对应的属性。第三步,改前端表单页,加一个输入框。这个流程走完后,你会发现修改是双向的——后端不需要改接口,因为新增表单提交时后端通过实体类接收未知字段会自动忽略,真正的改动只在前端。但如果要增加一个完整的“报价单”模块,那是另一个故事,需要从建表、写Mapper、写Service、写Controller、写前端页面一路走下来。这个过程中注意保持四层架构的代码风格与源码一致,不要自己另起炉灶。
// 推荐:新增查询接口时保持分页参数风格一致 public Page<CrmCustomerVO> queryCustomerPage(CustomerQuery query) { Page<CrmCustomer> page = new Page<>(query.getPageNum(), query.getPageSize()); // 条件构造器 LambdaQueryWrapper<CrmCustomer> 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路径拼接。
本文还有配套的精品资源,点击获取