资讯动态

SpringBoot+Vue3+MyBatis+MySQL 从零搭建企业级CRM客户管理系统

发布时间:2026/9/9 2:45:50 来源:尧图企业网站定制
1. 项目概述与整体设计思路1.1 这个系统到底解决了什么问题先说结论这是一套面向小型企业的客户关系管理系统CRM技术栈锁定在 Java 生态最主流的组合——SpringBoot Vue3 MyBatis MySQL前后端完全分离。核心目标是帮助企业把散落在销售个人微信、Excel 表格、纸质笔记本里的客户信息收拢到一个系统里统一管理客户资料、跟进记录、合同状态和交易数据。小企业的痛点其实很真实销售离职带走客户、跟进到一半忘了下次回访时间、老板想看看这个月成交了多少但数据统计要等销售手动汇总。这套系统就是在解决这些事。它不是那种动辄几十万的企业级 CRM 缩水版而是从零搭建、自己能掌控全部代码、想改哪里改哪里的轻量方案。对于正在学 Java 或者刚工作一两年的开发者来说这个项目的价值不只是能跑起来更重要的是它把三条主线串在了一起SpringBoot 怎么组织后端接口、MyBatis 怎么和 MySQL 交互、Vue3 怎么把数据渲染成用户能操作的界面。这三条线任何一条单独学都很枯燥放在一个具体业务场景里反而容易理解。1.2 为什么选这套技术栈而不是别的选型这件事我拆开讲一下当时的考量。后端用 SpringBoot 没什么悬念。它是目前 Java 后端的事实标准内嵌 Tomcat、自动配置、起步依赖这些机制让项目初始化成本极低。而且小公司招人也好、自己后面维护也好SpringBoot 的人才池子最大遇到问题搜解决方案也最容易命中。数据库选了 MySQL 8.0原因更简单免费、稳定、用的人多。小型企业的数据量撑死几百万行MySQL 完全能扛住。选 8.0 而不是 5.7主要是考虑了字符集默认就是 utf8mb4、窗口函数这些新特性的支持后面要做排行统计之类的 SQL 会顺手很多。MyBatis 和 JPA 之间我纠结过一阵。JPA 开发效率高但 SQL 可控性差复杂查询调优很痛苦。MyBatis 恰恰相反SQL 自己写、执行计划自己控制对于 CRM 这种查询条件经常动态变化的场景更合适。另一个现实因素是国内中小公司用 MyBatis 的存量项目太多了MyBatis 相关的面试题也多到可以单独出一本书。学这个对求职是加分项。前端选 Vue3 主要是看中组合式 APIComposition API带来的代码组织能力。CRM 的页面有很多状态联动比如客户详情页要同时展示基础信息、跟进记录、合同列表用传统的选项式 API 写逻辑分散在各个 data、methods 里代码一长就难维护。Vue3 的 setup 语法能把关联逻辑收拢在一起简直是这个场景的黄金搭档。配合 Vite 做开发服务器热更新速度比 Webpack 时代快了一个量级日常开发体验非常好。1.3 核心功能模块拆解这套 CRM 我按业务角色把功能拆成了四个核心模块外加一个系统管理模块兜底。客户管理是最基础的模块负责客户信息的增删改查支持名称、行业、来源、等级、状态等多条件组合筛选。客户状态我设计成了潜在客户、意向客户、跟进中、已成交、已流失五个阶段每个阶段可以配置对应的跟进策略。跟进管理负责记录每一次和客户的互动情况包括沟通方式、沟通内容、下次跟进时间。这个模块是整个 CRM 的黏合剂它让谁在什么时候和客户说了什么变得可追溯。销售换人了新接手的人打开系统就能看到完整的历史记录客户不至于因为人员变动而流失。合同管理处理成交环节记录合同金额、签约日期、付款方式、回款计划。系统会根据回款日期自动生成待办提醒尽量避免签了合同但钱迟迟没到账的情况。数据统计模块面向管理者展示客户总数、新增客户趋势、成交转化率、回款金额等关键指标。我用 MyBatis 写了几条聚合查询配合 Vue3 里的 ECharts 图表老板打开首页就能看到本月经营概况。系统管理模块包含用户管理、角色管理和权限分配。权限这块用了 RBAC 模型——用户挂角色、角色挂权限后端在拦截器里校验接口权限前端通过路由守卫控制页面访问。小公司通常不需要太复杂的权限粒度到按钮级别就够用了。2. 数据库设计与后端核心实现2.1 表结构设计的几个关键决策数据库设计是这类管理系统最见功力的地方。我最终落了六张核心业务表客户表 customer、联系人表 contact、跟进记录表 follow_record、合同表 contract、回款计划表 payment_plan、用户表 sys_user再加三张系统辅助表角色表 sys_role、权限表 sys_permission、用户角色关联表 sys_user_role。客户表是最核心的字段设计时重点考虑了两个问题。第一是客户归属用 owner_id 字段记录当前负责销售的 ID查询时直接用这个字段过滤数据范围。第二是客户去重通过 company_name 加行业字段做唯一性校验避免同一个客户被多个销售重复录入。这个校验不放在数据库层面而是在 Service 层先查一遍因为客户名称偶尔会有细微差别比如某某科技有限公司和某某科技公司完全靠数据库唯一索引容易误杀。跟进记录表 follow_record 设计了一个细节每条跟进记录都冗余存了当时的客户阶段 follow_stage。这样做的目的是方便后续统计分析——要查从初次接触到成交平均需要跟进几次直接看跟进记录表就够不用去关联客户表的当前状态。合同表和回款计划表是一对多的关系。系统里一个合同可能分多期回款所以 payment_plan 通过 contract_id 关联合同。设计时我在两张表都加了 index回款计划表在 due_date 字段上也建了索引因为按到期日扫描待回款列表是高频查询没有索引的话数据一多就很慢。2.2 MyBatis 与 MySQL 交互的实操细节MyBatis 的使用上我踩过不少坑这里挑几个重点说。第一个坑是主键回填。插入客户数据时业务上需要立刻拿到自增主键去关联联系人或者跟进记录如果配置文件里没设置 useGeneratedKeys插入后拿到的 ID 永远是 null。正确做法是在 insert 语句的标签上加上 useGeneratedKeystrue keyPropertyidMyBatis 会在执行插入后把数据库生成的主键值回填到传入对象的 id 字段里。第二个坑是动态 SQL 的条件拼接。CRM 的客户列表查询有七八个可选筛选条件客户名称模糊查询、行业精确匹配、状态枚举匹配、归属人匹配等如果用字符串拼接 SQL不仅容易出 SQL 注入漏洞而且代码丑陋。MyBatis 的 where if 标签组合可以优雅解决where 标签会自动去掉第一个多余的和或者或者之类的关键字if 标签根据条件是非空判断是否拼入片段。下面是客户列表查询的 XML 片段这种写法是 MyBatis 最经典的用法select idselectCustomerList resultTypecom.example.crm.entity.Customer SELECT * FROM customer where if testkeyword ! null and keyword ! AND (company_name LIKE CONCAT(%, #{keyword}, %) OR contact_name LIKE CONCAT(%, #{keyword}, %)) /if if testindustry ! null and industry ! AND industry #{industry} /if if teststatus ! null AND status #{status} /if if testownerId ! null AND owner_id #{ownerId} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /selectLIKE 查询这里用 CONCAT 拼接百分号而不是直接写 %${keyword}%是为了防止 SQL 注入。${} 是文本替换#{} 是预编译参数占位前者有注入风险能不用尽量不用。第三个坑是 MyBatis 的一级缓存。默认情况下 MyBatis 一级缓存作用域是 SqlSession同一个 SqlSession 内执行两次相同查询会命中缓存。听起来挺好但小坑在于如果你在一个事务里先查了一个客户然后另一个线程修改了这个客户的数据并提交你当前这个事务再次查询拿到的还是旧数据。这就是所谓的一级缓存脏读问题。解决方式有三种每次查询后手动清理缓存、把一级缓存策略改成 STATEMENT 级别、或者干脆在需要强一致性的地方用 Select 注解配合 flushCachetrue。我在实际项目里大部分场景不需要这种强一致所以保留默认配置就好但在合同回款这种涉及金额的场景我会显式加上 flushCachetrue。2.3 后端分层与 JWT 认证的实现思路后端代码按经典三层结构组织Controller 层负责接收请求和参数校验Service 层处理业务逻辑Mapper 层通过 MyBatis 操作数据库。每一层之间用阿里巴巴的开发规范约束——Controller 不直接调 MapperService 之间可以互相调用但禁止循环依赖。认证方案选了 JWTJSON Web Token而不是传统的 Session。原因有三点前后端分离后后端接口可能要同时服务 Web 端和未来的移动端JWT 天然无状态不需要 Session 同步小型系统没必要引入 Redis 做分布式 SessionJWT 把用户信息直接编码在 Token 里减轻了服务端内存压力JWT 本身携带过期时间适合做登录态失效控制。JWT 的核心代码逻辑不复杂就是生成 Token 和校验 Token 两步。生成时把用户 ID、用户名、角色编码放进 payload用 HMAC-SHA256 算法签名设置过期时间 24 小时。校验时在拦截器里解析 Token验签通过后把用户信息放入 ThreadLocal后续业务代码直接从 ThreadLocal 里取当前登录人不用每个接口都传用户 ID 参数。登录接口的密码校验用的是 BCrypt 加密。这种算法比 MD5 安全得多它内置随机盐值同一个密码每次加密结果都不一样攻击者即使拿到数据库也没法直接反推明文。开发时最容易犯的错误是拿加密后的密文去和明文比较正确做法是用 BCryptPasswordEncoder.matches(rawPassword, encodedPassword) 方法做校验。这里有个实用的编码经验统一封装一个 Result 响应体包含 code、message、data 三个字段。正常返回 code200业务异常返回 code500未登录返回 code401。前端 axios 拦截器里统一处理看到 401 就跳转登录页。这种约定一旦建立前后端联调时能省掉大量沟通成本。2.4 MyBatis 与 MyBatis-Plus 的选择纠结写这套系统时有不少朋友问我这个项目为什么不直接用 MyBatis-Plus它帮我们封装了单表 CRUD不用写 XML开发速度明显更快。我说说我的看法。MyBatis-Plus 确实提高了开发效率内置的 BaseMapper 提供了 insert、updateById、selectPage 这些现成方法还带代码生成器能直接从数据库表生成实体类、Mapper 接口、Service 层。但正因为太方便很多新手用了 Plus 之后连基础 SQL 都不会写了。CRM 这项目里核心的查询都在多表关联和动态条件上这些 Plus 也得靠 Wrapper 或者 XML 实现单纯依赖 Plus 省不了多少事。反而自己动手写 MyBatis 的 XML能把 SQL 的执行逻辑掌握得更透。如果是一次性交付给客户的外包项目追求速度我建议用 MyBatis-Plus。但如果是学习性质的项目或者自己打算长期维护、后面还要在它基础上叠加更多复杂报表的业务系统我更推荐原生 MyBatis至少先把 SQL 功底练扎实。这套 CRM 我把生成代码的脚手架放出来了后面要切换到 Plus 也不难Mapper 接口上加个继承 BaseMapper实体上加几个注解就能逐步迁移。3. 前端 Vue3 架构与核心页面实现3.1 Vue3 项目搭建与目录结构前端项目用 Vite 做构建工具创建命令很简洁npm create vitelatest crm-web -- --template vue整套前端目录按业务模块划分核心结构是这样的src/ api/ # 接口请求封装按模块拆文件 customer.js contract.js auth.js assets/ # 静态资源 components/ # 公共组件 Pagination.vue CustomerForm.vue router/ # 路由配置 index.js stores/ # Pinia 状态管理 user.js views/ # 页面组件 login/ dashboard/ customer/ contract/ utils/ # 工具函数 request.js # axios 封装 auth.js # token 存取接入 Vue Router 4 和 Pinia 作为路由和状态管理方案。选 Pinia 而不是 Vuex 的原因很直接Pinia 的 API 更简洁、对 TypeScript 支持好、没有 mutations 的概念直接改 state、支持组合式写法和 setup 语法天然契合。Vue3 官方文档现在也把 Pinia 作为推荐方案已经算是事实标准了。3.2 axios 封装与 Token 注入前端所有接口请求都走一个统一的 axios 实例这么做有两个好处一是拦截器统一处理 Token 注入和错误码提示二是方便切换开发环境和生产环境的基础路径。axios 封装的核心逻辑是这样请求拦截器里从 localStorage 取 Token有就加到请求头的 Authorization 字段响应拦截器里根据后端约定的 code 做统一处理code 为 200 正常返回数据code 为 401 清除登录态并跳转登录页其他错误码用 Element Plus 的 ElMessage 弹出错误信息。一个容易忽略的细节是 Token 失效的处理。JWT 过期后后端返回 401axios 如果没做拦截用户会看到接口报错的原始信息体验很差。我处理的方式是在响应拦截器里判断 HTTP 状态码遇到 401 就提示登录已过期请重新登录然后清理本地缓存并跳转登录页。这里要注意防止多个接口同时返回 401 导致重复跳转所以加了一个标志位控制跳转前先看看当前是否已经在登录页是的话就不再重复跳。3.3 客户管理页面的核心实现客户列表页是前端投入代码量最大的页面它同时用到了组合式 API、响应式数据和组件通信。客户列表的筛选区是一个表单包含关键字输入框、行业下拉框、状态下拉框、归属人下拉框和查询重置按钮。点击查询时触发一个 loadData 方法把表单数据组装成查询参数传给后端接口。这里我用了 computed 配合路由做搜索条件持久化。搜索条件放在 URL 的 query 参数上一方面刷新页面后条件不会丢另一方面可以直接复制带条件的 URL 发给同事。实现方式是监听路由变化重新加载数据把筛选表单的初始值从 this.$route.query 里读取。这个小特性看着不起眼实际用起来非常顺手。客户列表用的是 Element Plus 的 el-table 组件列包含公司名称、联系人、电话、行业、状态标签、负责人、创建时间和操作按钮。状态字段显示用了自定义的标签颜色映射比如已成交显示绿色、跟进中显示蓝色、已流失显示灰色。这个映射关系我专门抽成了一个小工具函数。新增和编辑客户共用一个 CustomerForm 组件通过 props 传入一个 customer 对象父组件控制弹窗的显示隐藏。编辑时把整行数据传给子组件子组件 watch props 变化回填表单新增时传空对象表单所有字段为空。这里有个 Vue 的经典坑如果直接传同一个对象给子组件子组件内部修改了 props 会导致父组件数据也跟着变。我的处理方式是让子组件用 computed 创建一个本地副本在保存时再通过 emit 把最终数据抛给父组件。删除客户的操作加了二次确认弹窗使用 ElMessageBox.confirm 实现确认后调删除接口成功后刷新列表。这个交互设计虽然简单但能防止误删操作实际使用中价值很大——客户数据一旦删除很难恢复比合同数据更需要保护。3.4 路由守卫与权限控制前端权限控制的思路是登录成功后后端返回当前用户的角色和权限列表前端把权限列表存到 Pinia路由守卫里判断目标路由需要的权限码是否在当前用户权限集合中。路由配置里每条路由可以加一个 meta 字段存放 roles 权限码。全局前置守卫 beforeEach 里做三步判断检查是否已登录没登录就跳转登录页已登录但访问的是登录页重定向到首页已登录访问其他页面检查 meta.roles 是否包含当前用户角色不包含就提示无权访问并重定向到 403 页面。按钮级别的权限控制用 Vue3 自定义指令实现。我封装了一个 v-permission 指令指令的值是权限码如果当前用户没有该权限码就移除按钮的 DOM 元素。这种细粒度控制在 CRM 里很有用比如普通销售看不到合同审批按钮只有销售经理角色才有。核心实现代码如下const permission { mounted(el, binding) { const requiredPerm binding.value const userStore useUserStore() if (requiredPerm !userStore.permissions.includes(requiredPerm)) { el.parentNode el.parentNode.removeChild(el) } } }路由守卫和指令权限是同一套权限数据的两种用法一个控制页面访问一个控制操作按钮显示双保险防止越权操作。4. 环境准备与本地部署实操4.1 从零开始装 Java 和配置环境变量很多新手卡在项目跑不起来的第一关就是环境变量。JDK 装完之后如果不配环境变量命令行里敲 java -version 会直接提示找不到命令。JDK 我推荐用 17 或者 21SpringBoot 3.x 要求最低 Java 17。下载后解压或者安装到一个路径比如 Windows 下装在 D:\Java\jdk-17然后在系统环境变量里做三件事JAVA_HOME 设置为 D:\Java\jdk-17PATH 里新增一条 %JAVA_HOME%\bin打开命令行输入 java -version 验证PATH 里加 %JAVA_HOME%\bin 的目的是让操作系统能在任意目录下找到 java.exe 和 javac.exe。验证方式很简单新开一个命令行窗口注意是新的旧窗口不会刷新环境变量输入 java -version能看到版本号就说明配置成功。如果提示找不到命令多半是 PATH 没生效或者 JAVA_HOME 路径写错了。IDEA 里也要确认 Project Structure 的 SDK 设置了正确的 JDK 版本Maven 的 Settings 里如果配置了镜像源记得用阿里云的镜像仓库否则第一次拉取依赖会非常慢。4.2 MySQL 8.0 安装与初始化配置MySQL 8.0 的安装Windows 上最简单的方式是用官方安装包选 Developer Default 组件即可。安装过程有几个坑一是密码设置要选对认证方式8.0 默认是 caching_sha2_password但这个认证方式在有些老版本的 JDBC 驱动下连不上需要在建完用户后把认证插件改成 mysql_native_password或者直接在安装时选 Legacy Authentication。现在新的 MySQL Connector/J 8.x 都支持新认证了直接用默认即可。二是安装完成后要在配置文件 my.ini 里加一行 lower_case_table_names1。这个参数控制表名大小写是否敏感Windows 上默认是 0Linux 上是 0如果开发环境是 Windows、生产环境是 Linux两边大小写策略不一致代码里表名的大小写写法在不同环境可能出现问题。统一设置成 1 表示表名不区分大小写能避免很多莫名其妙的表不存在报错。连接数据库时我推荐先用命令行确认 MySQL 服务正常mysql -u root -p能登录进去之后再在 IDEA 里配置 DataSourceURL 写法是jdbc:mysql://localhost:3306/crm_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue这里 serverTimezone 必须设置否则 Java 连接 MySQL 8.0 会因时区问题报错。allowPublicKeyRetrievaltrue 也是新版本连接器需要的参数不配置会提示 Public Key Retrieval is not allowed。数据库初始化我写好了两份文件schema.sql 负责建表、data.sql 负责插入初始数据默认管理员账号 admin/admin123。项目启动前在 IDEA 的 Database 工具或命令行执行一次就行。4.3 后端与前端项目启动全流程后端启动相对简单用 IDEA 打开后端项目等待 Maven 下载完依赖然后运行 CrmApplication 主类。如果正常启动控制台会输出 SpringBoot 的启动日志看到Started CrmApplication字样就说明后端起来了。前端启动需要先安装 Node.js 18 以上版本然后进到 crm-web 目录执行npm install npm run devnpm install 第一次会下载几百 MB 的依赖耗时取决于网络。如果卡住不动可以换淘宝镜像源npm config set registry https://registry.npmmirror.com。前端开发服务器默认跑在 5173 端口Vite 开发服务器自带代理功能我在 vite.config.js 里配置了 /api 前缀的请求转发到后端的 8080 端口这样前端代码里请求路径写 /api/customer/list开发环境下就自动代理到后端接口不需要关心跨域问题。生产部署建议用 Nginx 托管前端静态文件再反向代理后端接口。后端打包用 mvn clean package -DskipTests 生成 jar 包然后 java -jar crm-web-1.0.0.jar 启动。麻雀虽小五脏俱全这套部署流程放到一台 2 核 4G 的服务器上跑个几十人规模的公司绰绰有余。5. 常见问题与排查技巧实录5.1 前后端联调时的经典报错第一个经典报错是跨域。前端页面跑在 5173 端口后端接口在 8080 端口浏览器会拦截跨域请求。如果前端没有配置代理、后端也没开 CORS打开页面时接口请求会直接失败控制台报错明确写着 blocked by CORS policy。排查时可以先用 Postman 或 apifox 直接调后端接口确认接口本身没问题再看是不是 CORS 配置的问题。后端的解决方案是加一个 WebMvcConfigurer 的配置类或者直接在启动类上 CrossOrigin。我的项目里两种方式都支持开发环境优先用前端 Vite 代理生产环境用 Nginx 反向代理后端只对白名单内的域名开放 CORS。第二个经典报错是日期格式。MySQL 里的 datetime 类型传到前端变成了一串时间戳页面上显示成数字。这个问题的根源在于 Jackson 序列化 LocalDateTime 时默认输出的是时间戳数组。我一开始踩了这个坑页面表格里看到的是一个数组结构后来在 application.yml 里配置了统一的日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时把 Java 时间字段的类型统一用 LocalDateTime这样后端返回的都是格式化好的字符串前端直接渲染即可。前端没有额外接转换层这也是省事的一种做法。第三个经典报错是自增主键 ID 的精度丢失。MySQL 的 BIGINT 类型对应的 Java 类型是 Long这个没问题但传到前端之后JavaScript 对超过 JavaScript Number 安全整数范围2^53 - 1的数字会丢失精度。客户 ID 如果超过这个范围实际上自增主键很少到这么大但 Snowflake ID 算法生成的 ID 就非常容易触发这个问题。解决办法是在后端返回 JSON 之前把 ID 转成字符串或者前端用 string 类型接收。我直接在后端统一加了 Jackson 的 ToStringSerializer 处理 Long 类型 ID 字段JsonSerialize(using ToStringSerializer.class) private Long id;前端核对 ID 时永远当字符串处理就没有精度问题了。5.2 MyBatis 动态 SQL 的隐性坑动态 SQL 里最容易踩的坑是多余逗号问题。写批量插入或者带条件更新时如果 if 标签组合不好生成出来的 SQL 可能带着多余的逗号。比如更新语句里最后一个 set 字段后面不该有逗号但如果这个字段恰好被 if 跳过就会导致语法错误。MyBatis 的标签就是专门处理这个问题的它会自动去除最后一个条件之后的逗号。同理 标签自动处理 AND 前缀。这是 MyBatis 官方推荐的标准做法比手动判断第一个条件是否成立要可靠得多。另一个坑是 MyBatis 一级缓存导致的重复查询数据不一致。我排查过一次诡异现象同一个请求里第一次查客户列表是有数据的第二次查同一个客户详情却返回了 null。排查了半天发现是服务方法里两个查询操作使用了同一个 SqlSession而第一次查询后有人删了这条数据一级缓存命中的结果却还是旧数据。解决办法是在需要实时数据的地方把 Transactional 放在更精确的方法上缩小事务范围。5.3 性能排查与 SQL 优化心得CRM 系统数据量到几十万条后列表查询会明显变慢。排查思路是先看 SQL 执行计划。MySQL 里 EXPLAIN SELECT 关键词看 type 字段是 ALL全表扫描还是 range、ref走索引。如果 type 是 ALL 并且数据量大基本可以断定是索引缺失。我实际优化过几个点。第一是客户列表的排序字段 create_time 加了普通索引排序查询从一两秒降到几十毫秒。第二是跟进记录表按 customer_id 建了索引客户详情页加载跟进记录的速度提升明显。第三是列表分页的深翻页问题页码大了之后 LIMIT 100000, 20 需要扫描前十万行效率很低。优化方式是改成基于游标的分页上一页最后一条记录的 ID 作为查询条件WHERE id ? ORDER BY id DESC LIMIT 20。这类优化在数据量还不大的阶段可以先不做但如果你的客户量涨得快可以说做就做。5.4 前端常见的请求与渲染问题axios 请求发送后没有反应大概率是请求被代理拦了或者路径写错。排查方式是打开浏览器开发者工具的 Network 面板看请求实际发出的地址和响应状态码。如果 404 就是路径不匹配如果 500 就是后端代码有问题如果请求挂在 pending 状态很久没响应多半是后端接口超时或数据库连接池满了。表格数据渲染不出来的另一个常见原因是字段名对不上。后端返回的字段是下划线命名 create_time前端模板里写的是驼峰 createTime拿到数据后无论如何渲染都是 undefined。解决方案是前后端约定一套命名规则要么后端全局配置 map-underscore-to-camel-casetrueMyBatis 自动把下划线字段映射成驼峰属性要么前端在拿到数据后做一层格式化转换。我更推荐前者因为 MyBatis 全局开启这个配置后实体类属性统一用驼峰命名即可两边的代码都很干净。6. 项目扩展方向与我的实操体会6.1 后续还能怎么扩展这套系统完成后我自己又往上叠加了几个实际用得上的功能给大家参考。多租户支持如果想把系统卖给你的几个客户公司用SaaS 化需要在所有业务表加一个 tenant_id 字段查询时自动追加过滤条件。这个改动不大但设计思路要从单公司模型转向多公司模型涉及用户注册、数据隔离、计费模式等一系列变化。我建议先别急等真的有客户需求了再动这个模块否则容易过度设计。消息通知CRM 最实用的扩展是把下次跟进时间到了这种事件主动推给销售。实现方式有两种后端定时任务扫描 overdue 记录通过 Server-Sent EventsSSE或 WebSocket 推送给前端或者接入企业微信/钉钉的机器人 webhook把提醒发到群里。我自己用了后一种基于 SpringBoot 的 Scheduled 定时注解每天晚上八点扫描第二天的待跟进和待回款任务拼成文本推到钉钉群。这个功能上线后销售们各个好评基本不用主动打开系统就能知道当天要干哪些事。导入导出Excel 导入客户、导出合同列表这种需求在 CRM 里几乎是必点的。后端用 EasyExcel 或者 Apache POI 都行前端配合 Element Plus 的上传组件实现成本不高但实用价值很大。销售们手里积压的大量 Excel 客户数据终于有了一条数据进入系统的路径不至于每次都手工录入。6.2 我踩过的几个最有价值的坑第一个是事务切面不生效的问题。某个删除客户的接口同时要删客户数据、跟进记录、联系人我在 Service 方法上加了 Transactional 注解自认为没问题。但真实代码里有个坑这个方法是在同一个类的另一个方法里被 this 调用的Spring 的 AOP 代理默认只拦截外部调用类内部调用 this.method() 不会触发事务。解决办法是拆分出一个单独的 Service 类或者注入自身的代理对象调用。这种问题不好排查往往要等到真正出现数据不一致的时候才恍然大悟。第二个是 MySQL 连接断开后的自动重连。服务器上跑久了如果 MySQL 的 wait_timeout 设置了比较短的时长Java 应用里空闲的数据库连接会超时断开。再次使用时如果连接池没做好探活就会报 Communications link failure。解决方式是在连接池配置里加上 testWhileIdletrue、validationQuerySELECT 1并且把连接池的 max-lifetime 设置为小于 MySQL wait_timeout 的值。这个配置一般不会出现在教程里但生产环境一定会遇到。第三个是前端打包后白屏问题。Vue3 项目 npm run build 之后本地确认没问题上传到服务器访问却白屏。打开控制台发现资源路径 404。原因是 Vite 默认配置 base 为 /打包生成的静态资源路径是根目录相对路径部署到子路径时文件就找不到了。解决办法是在 vite.config.js 里把 base 改成 ./让所有的资源引用变成相对路径这样无论部署在哪个子路径都能正确加载。6.3 基于这套技术栈的几点学习建议如果你是完全的新手刚接触 SpringBoot 和 Vue3建议先别急着把整个项目跑起来而是按模块拆理解第一步把后端单独的接口调通用 Postman 测试客户增删改查接口第二步把前端单独的静态页面搭起来用假数据渲染列表第三步再通过 axios 把前后端连通联调一个完整流程。这三步做完你对前后端分离的理解会非常具体。如果一上来就想着整体跑通遇到任何问题都可能是后端原因、前端原因或者配置原因排查起来没有头绪容易劝退。工作上如果要把这套架构落地到公司实际业务有几个点必须提前和业务方达成一致客户数据的录入和更新频率、客户归属调整的流程、合同金额和回款计划的审批权限。这些业务规则直接决定了数据表怎么设计、后端接口怎么拆分、前端页面怎么排版。我见过不少开发者和业务方在系统上线后才讨论这些结果做出来一堆返工需求。前期多沟通一次后面能少改十次。这套系统的价值不在于代码有多精妙而在于它是一个完整闭环从前端的页面交互到后端的接口逻辑再到数据库的表结构设计三者是有机结合的。理解了这层联系不管是做 CRM 还是以后做其他管理系统你都能整套迁移过去。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价