尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SpringBoot+Vue3+MyBatis的前后端分离CRM系统实战

基于SpringBoot+Vue3+MyBatis的前后端分离CRM系统实战 我们的销售管理一直靠Excel表格撑着客户信息散落在不同同事手里交接全靠微信群聊和口头传达。有一次谈了两个月的重点项目因为跟进记录没同步差点跟丢客户那次之后我下定决心自己搭一套客户关系管理系统。前后花了两周业余时间用SpringBootVue3MyBatis把前后端分离的CRM系统从零写了出来数据库用的MySQL。需要声明一下这不是什么高大上的商业系统而是一套能跑、能管客户、能记录跟进、能统计数据的完整源码适合中小团队自用也适合刚学完SpringBoot和Vue3的开发者拿来练手。这套系统的核心价值在于后端被拆分成清晰的分层结构前端完成了工程化搭建对接了Element Plus组件库把客户、联系人、跟进记录、数据看板四个模块完整串了起来。如果你是正在学Java技术栈的开发者想看看一个完整的前后端分离项目到底怎么落地参考这份源码是再合适不过的路径。也是因为这套系统我在实际开发中踩透了SpringBoot事务管理、MyBatis动态SQL、Vue3响应式数据这一连串企业开发的常规操作一点都不虚。1. 项目整体设计与技术选型1.1 核心需求与功能拆解做系统之前我先把销售管理的痛点梳理了一遍其实就四件事。第一客户资料不能散得有统一的录入和查询入口。以前客户信息都记在个人通讯录、微信笔记、甚至纸片上面整理了也白整理。所以我需要一个客户表支持多条件组合搜索比如按客户名称、所属行业、客户状态筛选还要支持分页。第二跟进记录要有痕迹谁在什么时间联系了哪个客户、聊了什么、下一步计划是什么这些都要结构化地存下来。销售团队里经常出现客户被重复跟进或者漏跟的情况就是没有电子化的跟进记录造成的。第三联系人得跟客户关联起来一个客户公司底下可能有采购经理、技术负责人、财务总监三个联系人他们要挂在同一个客户下统一管理。第四管理层要看数据每周跟进了多少客户、商机分布在哪个阶段、业绩大概怎么样不能靠拍脑袋。做一张数据看板把客户总数、本周新增客户数、本周跟进次数、商机阶段分布这些指标直接展示出来。基于这四块需求我把整个系统拆成了客户管理、联系人管理、跟进记录、数据统计四个核心模块外加一个用户登录模块做权限入口。功能范围控制在一个合理的规模内既覆盖了销售管理的基本流程也不至于让初学的人一头雾水。1.2 技术栈选型为什么是SpringBootVue3MyBatisMySQL选这套组合我是有实际考虑的。后端用SpringBoot本质上就是看中它的自动装配能力和生态成熟度。SpringBoot把SpringMVC、事务管理、连接池、日志这些基础设施都整合好了我只需要关注业务代码本身。版本上我选了2.7.18这个版本在稳定性和兼容性之间比较平衡能兼容JDK8和JDK11也支持后续升级到3.x。如果你用的是SpringBoot 3.x需要注意JDK版本必须到17以上javax包名也要改成jakarta这个坑后面详谈。ORM框架我选了MyBatis而不是MyBatis-Plus是因为CRM系统里的查询逻辑虽然不算特别复杂但多表联查和按条件动态拼接SQL的场景非常多。客户查询要拼行业条件跟进记录查询要关联客户表数据统计要写GROUP BY。MyBatis的XML Mapper在这种场景下极其灵活SQL写得直接方便优化和调优。如果你业务简单、都是单表操作选MyBatis-Plus能省不少事但想锻炼SQL能力、想在复杂查询上游刃有余MyBatis是绕不开的基础功这也是为什么我强烈推荐用原生MyBatis来搭这套系统。前端选Vue3配Vite构建工具核心原因是开发体验好。Vite基于ES Module启动项目基本秒开热更新也飞快。Vue3的Composition API让代码组织更灵活逻辑可以按功能拆进独立的函数里不像Vue2的Option API一样所有东西都要塞在data、methods、computed里。UI组件库配了Element Plus表格、弹窗、表单、分页这些后台管理页面的常见组件都有现成的加上表单校验功能也齐全能大幅缩短开发周期。数据库选MySQL 8.0这是目前最流行、资料最多的开源关系型数据库。8.0版本默认字符集是utf8mb4支持存储emoji和特殊字符也支持窗口函数对统计类SQL帮助很大。CRM系统并发量不会特别大MySQL完全够用而且部署成本低一台服务器跑起来很轻松。整体系统的数据访问关系是Vue3应用通过HTTP调用后端RESTful APISpringBoot控制器接收请求Service层处理业务逻辑Mapper层封装MyBatis的SQL访问逻辑底层连接MySQL数据库。请求响应统一采用JSON格式前端通过Axios封装请求库与后端通信。这也是目前主流的前后端分离架构模式一套系统搞明白后面遇到类似项目都能复用这套思路。2. 数据库设计与后端核心实现2.1 数据表结构设计数据库设计是整套系统的地基。我设计了一个名为crm_db的数据库里边一共四张表sys_user、customer、contacts、follow_up_record。sys_user表管登录用户核心字段有id、username、password、real_name。密码存储我用的是MD5加密后再加盐处理实际项目我建议你换成BCrypt或其他更安全的加密方式毕竟MD5在碰撞攻击面前已经不安全了。customer表存储客户主要信息我设计了这些字段id、customer_name客户名称、industry所属行业、level客户级别比如A类重点客户、B类普通客户、status状态潜在客户、跟进中、已成交、已流失、source客户来源、phone联系电话、address地址、owner_id归属人ID关联sys_user表、create_time、update_time、remark。其中owner_id很关键有了它以后才能做数据权限——每个销售只能看自己名下的客户这个字段给后续扩展留了口子。contacts表存联系人重要字段有id、customer_id关联customer表、contact_name联系人姓名、position职位、phone联系电话、email、wechat、is_primary是否首要联系人。这里的customer_id是外键逻辑我写了外键约束确保数据一致性。联系人表是一对多关联客户表一个客户下有多个联系人删一个客户时他的联系人和跟进记录要注意处理方式我采用的是软删除思路保持数据可追溯。follow_up_record表存跟进记录核心字段是id、customer_id关联客户、contact_id关联联系人、content跟进内容、next_plan下一步计划、follow_up_time跟进时间、creator创建人。这张表对销售管理至关重要是所有业务分析的基础数据源数据量增长也最快后面做数据统计全靠它。设计表结构时有几个细节需要注意。一是所有表的id都用BIGINT类型因为主键的值可能会超过INT范围。二是create_time和update_time这两个时间字段统一用datetime类型不要用timestamp避免2038年问题。三是每条业务表都预留一个remark字段别小看它等业务上线后你会发现评论和备注类的需求特别多有字段总比后期改表结构要方便。具体的建表SQL我在源码里附了完整的脚本用Navicat或者MySQL命令行直接执行就行。2.2 SpringBoot工程搭建与MyBatis集成新建SpringBoot工程可以用Spring Initializrstart.spring.io选择Java版本、SpringBoot版本2.7.18勾选SpringWeb依赖然后手动引入MyBatis、MySQL驱动、Lombok、Druid连接池这几个依赖。这里我直接把pom.xml里的关键依赖列出来dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies之后在application.yml文件里做核心配置。端口选8080数据库连接池用Druid配置连接地址、用户名、密码。MyBatis部分两个关键配置一个是mapper-locations指定XML文件位置我用的是classpath:mapper/*.xml另一个是configuration.map-underscore-to-camel-case设为true这样数据库下划线字段能自动映射成Java驼峰字段类型为physical type。server: port: 8080 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/crm_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root druid: initial-size: 5 min-idle: 5 max-active: 20 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.crm.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl最后一个log-impl配置建议开起来开发阶段能在控制台看到MyBatis执行的具体SQL语句和参数值对排查问题帮助巨大部署上线前再把它关掉。启动类上记得加MapperScan注解扫描Mapper接口SpringBootApplication MapperScan(com.crm.mapper) public class CrmApplication { public static void main(String[] args) { SpringApplication.run(CrmApplication.class, args); } }2.3 后端分层架构与核心业务实现后端代码分成controller、service、mapper、entity四个包。Controller只负责接收请求、参数校验和返回统一结果Service层写业务逻辑Mapper是MyBatis的接口层Entity层是数据库表对应的实体类。统一返回结果我定义了一个Result类包含code、message、data三个字段。成功返回200业务异常返回400系统错误返回500。这样前端只用判断code就能知道接口执行状态不用每个接口单独处理不同的响应结构。以客户分页查询为例这是CRM系统最常见的接口。前端需要传pageNum页码、pageSize每页条数、还要带上customerName、industry、status等过滤条件。由于这些条件可能为空SQL必须做动态拼接。MyBatis的where标签和多if判断组合是处理这种情况的最佳方案效果是条件非空才拼进SQL全为空时SQL退化为最简单的全表分页查询。select idselectCustomerPage resultTypecom.crm.entity.Customer SELECT * FROM customer where if testcustomerName ! null and customerName ! AND customer_name LIKE CONCAT(%, #{customerName}, %) /if if testindustry ! null and industry ! AND industry #{industry} /if if teststatus ! null and status ! AND status #{status} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select同时还要写一个count查询统计满足条件的总记录数分页组件要拿这个总数来计算总页数。这里要注意count查询的条件和列表查询的条件必须保持一致否则页数会显示错乱。我的习惯是这两个SQL放一块写用注释标记清楚改一个忘改另一个的尴尬局面就不会出现。写客户新增接口的时候我加了一个经验customer_name不能为空同一归属人名下不能有重复客户。这个校验在Service层加了一段代码先查一次库判断同名客户是否存在存在就抛出业务异常。另外数据库层也要给customer表加上unique唯一索引兜底双保险防止并发情况下两条相同记录同时写入。联系人模块的核心逻辑比较简单关键操作是新增联系人的同时要判断customer_id是否存在避免挂到不存在的客户下面。更新联系人时用updateById类型的方法根据主键动态更新非空字段前端只传要修改的字段就能完成部分更新。跟进记录模块是业务核心。新增跟进记录时我用了一个方法并加了Transactional事务注解。一次完整的跟进动作包含两步操作往follow_up_record表插入一条跟进记录同时更新customer表对应的客户状态——如果客户原本是潜在客户跟进后要改成跟进中。这两个操作要么都成功要么都失败绝对不能出现记录写了但客户状态没更新这种情况。Transactional默认只在RuntimeException出现时回滚检查异常是不会触发的这是个容易踩的坑。自定义业务异常最好继承RuntimeException事务才有保障。2.4 数据统计模块的关键SQL数据统计是老板最关心的模块也是SQL最有发挥空间的地方。我写了三个统计接口每个都踩过坑。第一个是客户总数和本周新增客户数。客户总数好写SELECT COUNT(*) FROM customer就行。本周新增要算本周一到现在的新增记录用MySQL的YEARWEEK函数最方便SELECT COUNT(*) FROM customer WHERE YEARWEEK(create_time, 1) YEARWEEK(CURDATE(), 1)这里第二个参数1表示周一作为一周的第一天如果不写这个参数默认是周日输出结果对不上业务认知。这个细节很容易被忽视我一开始就栽在这里。第二个是本周跟进次数。需要按天展示趋势用日期函数分组SELECT DATE_FORMAT(follow_up_time, %Y-%m-%d) AS followDate, COUNT(*) AS count FROM follow_up_record WHERE YEARWEEK(follow_up_time, 1) YEARWEEK(CURDATE(), 1) GROUP BY DATE_FORMAT(follow_up_time, %Y-%m-%d) ORDER BY followDate前端拿到这个结果后遍历补全没有数据的日期数量填0就能画出连续的趋势图。第三个是商机阶段分布。统计不同状态的客户数量直接用GROUP BY statusSELECT status, COUNT(*) AS count FROM customer GROUP BY status后端返回数组前端直接渲染成饼图或者柱状图。这套系统我配了ECharts效果很好是展示客户分布的重要组件。3. 前端Vue3工程化实现3.1 Vite工程搭建与目录规划前端我用的Vite脚手架创建Vue3项目。执行命令npm create vitelatest crm-web -- --template vue然后安装依赖npm install npm install vue-router4 element-plus axios echarts这里有个经验要分享Vue3项目必须配vue-router的4.x版本3.x是配Vue2的版本装错会导致路由完全跑不起来。Element Plus同理也要用最新匹配Vue3的版本。前端目录结构我做了清晰拆分src/ api/ # 存放所有接口请求封装 router/ # 路由配置 store/ # Pinia状态管理 views/ # 页面视图 login/ # 登录页 layout/ # 主布局框架 customer/ # 客户管理页面 contacts/ # 联系人管理页面 followup/ # 跟进记录页面 dashboard/ # 数据统计看板 components/ # 公共组件 utils/ # 工具函数axios封装等这个结构是后台管理系统的经典模式页面按业务模块拆文件夹公共组件单独抽出来后续加功能只需要在对应模块下面加文件就行。刚开始学的时候很容易把所有组件堆在components里或者页面文件全部平铺放一堆项目一大就乱套了。分层清晰的项目结构从第一天就应该养成习惯。3.2 Axios封装与登录鉴权所有的HTTP请求我统一封装在utils/request.js里。Axios实例需要设置baseURL为/api配合后端接口路径。关键在于请求拦截器和响应拦截器。请求拦截器做token携带。用户登录成功之后后端返回一个token字符串前端把它存到localStorage中。每次发请求之前请求拦截器自动从localStorage取token加在请求头的Authorization字段后面。这样有权限校验的接口就能正常访问了。响应拦截器统一处理后端返回的响应。判断data.code是否等于200等于才正常返回不等于就弹出Message提示错误信息比如用户名或密码错误。如果遇到HTTP 401状态码说明token失效直接清除本地存储并跳回登录页。拦截器是处理统一逻辑的最好位置不用在每个页面单独判断token有效性。登录逻辑简单直接登录页拿到表单数据调用登录接口成功之后把用户信息和token存起来然后跳转到首页。为了保护需要登录才能访问的页面路由配置里我加了前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })这段代码的含义是访问登录页以外的任何页面只要本地没有token就直接踢回登录页这就是最基础的登录拦截方案。3.3 客户管理页面实现客户管理页面是前端工作量最大的部分。页面整体分为三个区域顶部的搜索表单中间的操作按钮下方的数据表格。搜索表单用Element Plus的el-form组件里面的输入框我用了el-input下拉框用el-select点搜索按钮时重新加载第一页数据点重置按钮时清空表单条件再刷新数据。实现搜索功能时搜索条件要绑定在一个叫queryParams的响应式对象上传给后端的分页查询接口。表格区域用el-table组件绑定客户数据列表表格列展示客户名称、所属行业、客户级别、状态、联系电话、创建时间。客户状态是枚举值直接显示数字会看得一头雾水我写了一个格式化函数0对应潜在客户、1对应跟进中、2对应已成交、3对应已流失配合el-tag组件渲染成带颜色的标签效果非常直观。列表底部是el-pagination分页组件页数变化时重新向后端发起请求。这部分的逻辑其实就是表单数据的变化触发数据重新加载理解了数据驱动视图的理念Vue3开发的核心就抓住了一大半。新增和编辑客户用同一个Dialog弹窗组件里面是表单校验。打开弹窗时判断当前是新增还是编辑模式编辑模式先把当前行的数据拷贝一份放到表单里用户点保存时再提交。这个操作有个很关键的细节编辑数据一定要浅拷贝一份不能在原对象上直接改否则弹窗还没保存表格里的数据就已经变了。这个bug看起来小但一旦线上出现数据错误排查起来会非常头疼。3.4 跟进记录与统计看板跟进记录页面是前后端交互最复杂的页面。因为每条跟进记录要同时展示客户名称和联系人姓名。列表中需要展示跟进的客户名称和联系人姓名。为了拿到这些信息后端查询SQL采用多表联查把跟进记录表和客户表LEFT JOIN再加上和联系人表的LEFT JOIN。前端展示这些数据的时候我用了el-timeline时间线组件按时间倒序展示每条跟进记录每条记录显示跟进内容、客户名、联系人、下次计划、创建时间。这个设计比表格更适合浏览业务跟进过程管理者看的时候视觉信息更清晰。时间线组件是Element Plus里边不太常用但非常好用的组件CRM系统用它来做跟进记录展示再合适不过。统计看板我用ECharts实现。页面挂载时同时调用三个统计接口拿到数据后用ECharts的折线图展示本周跟进趋势饼图展示客户状态分布卡片组件展示客户总数和本周新增客户数。ECharts初始化有个关键点图表必须在DOM渲染完成之后初始化用Vue3的onMounted生命周期钩子准没错。如果数据是异步加载的要等数据到了再setOption否则图表会空白。还有一个容易忽略的细节组件销毁的时候要调用dispose方法销毁ECharts实例否则重复进入页面会导致内存泄漏。4. 前后端联调与部署上线4.1 开发环境的跨域配置前后端分离开发模式下前端跑在5173端口后端跑在8080端口跨域问题躲不掉。最直接的方案是后端配置跨域过滤器允许前端域名访问所有接口。我在后端加了一个配置类注册CorsFilter允许的来源是全配还是指定根据实际情况来。开发阶段我配置的是http://localhost:5173如果之后有多个前端环境在跑也可以用allowedOriginPatterns方法做更灵活的模式匹配。要注意的是跨域配置放在网关或反向代理层也很常做但本地联调阶段后端统一配置最省事。更好的方案是配置Vite代理。修改vite.config.js文件把前端对/api的请求代理到后端服务的8080端口export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这个方案的核心优势是浏览器上看起来请求的是同源地址不存在跨域问题开发时调试更方便。production环境部署时则用Nginx做反向代理把/api路径的请求转发到后端服务前端静态文件直接由Nginx托管这是前后端分离项目最经典的生产部署方案。4.2 生产环境部署要点生产环境我推荐直接用Docker Compose来编排部署一套组合把MySQL、后端Java应用、前端Nginx服务全部管起来管理起来比手动安装省心很多环境隔离也好一台机器跑多个项目互不干扰。如果Docker不熟也可以直接在服务器上装MySQL、打一个jar包跑后端、再配个Nginx托管前端dist文件夹也不是不行只是手工操作步骤多、可重复性差。后端部署打包命令是mvn clean package执行完会在target目录生成crm.jar文件。运行时用java -jar crm.jar --spring.profiles.activeprod指定生产环境配置。生产环境的数据库密码、连接地址这些配置不要写在application.yml里用环境变量传参的方式更安全。SpringBoot支持使用${DB_PASSWORD}这种方式占位在服务器启动命令里通过环境变量注入进来的实际值能避免把密码硬编码进配置文件。前端构建命令是npm run build构建完会在dist目录生成纯静态文件把这个目录传给Nginx就行。Nginx的server配置大致如下server { listen 80; server_name your-domain.com; root /var/www/crm/dist; index index.html; location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这个配置里的关键点是location /api/规则前端发出的所有api请求都会被转发给后端的8080端口服务而静态页面资源会直接从前端dist目录返回。Nginx配好之后记得执行nginx -t检查配置语法再执行nginx -s reload平滑重载配置线上环境重启Nginx会导致短暂的连接中断能reload就别restart。4.3 前端静态资源优化生产环境前端页面的JS和CSS文件体积都不小Element Plus组件库本身就几百KB加上ECharts和其他依赖首次访问白屏时间会比较长。我对构建做了几个优化。首先是路由懒加载把每个页面的组件改成动态import引入。这样做的好处是首屏只加载当前页面需要的JS文件其他页面的代码在用的时候才加载const Dashboard () import(../views/dashboard/index.vue)其次是按需引入Element Plus组件配合unplugin-auto-import和unplugin-vue-components这两个Vite插件组件库中没用到的组件就不会打进最终的JS包里。ECharts体积大按需引入图表类型和渲染器也很有效import * as echarts from echarts/core import { BarChart, LineChart, PieChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent } from echarts/components import { CanvasRenderer } from echarts/renderers最后是压缩构建产物Vite默认开启了Gzip压缩但Nginx服务端也要开启gzip两个环节搭配起来效果才明显。前端构建产物从2MB压到不到600KB首屏加载时间从3秒多降到1秒以内用户体验直接上一个台阶。5. 常见问题与排查技巧实录5.1 高频报错问题速查表这个项目调试过程我积累了不少一手问题记录整理了高频报错和排查方面的心得列成一张表遇到同类问题可以对照着处理。问题现象根本原因解决方案前端请求后端404接口路径不匹配或者Vite代理路径配置错误检查后端Controller的RequestMapping路径和前端api目录中的请求路径是否完全一致日志里对照实际请求的URL数据库中文乱码MySQL连接URL缺少characterEncoding参数或数据库本身字符集不是utf8mb4在JDBC连接URL中强制指定characterEncodingutf8建库时指定utf8mb4字符集MyBatis报BindingExceptionMapper接口和XML文件没有正确绑定接口方法名与XML中id不匹配确认mapper-locations路径正确XML文件namespace指向完整接口名每条SQL的id对应接口方法名前端表格数据显示[object Object]字段名映射错误后端返回的JSON字段与前端绑定的prop不一致使用map-underscore-to-camel-case时检查数据库下划线字段与Java驼峰字段是否对应前端el-table-column的prop和返回数据字段名保持一致登录成功后刷新页面失效token只存在内存中没有持久化登录成功后localStorage.setItem保存token每次刷新时从localStorage中读取。所有请求在拦截器里统一携带token分组查询统计结果不对SQL中group by的字段包含NULL值或者数据类型不一致在group by之前用IFNULL处理NULL值用CAST保证参与分组的字段类型一致部署到服务器后上传文件失败Nginx默认限定了客户端请求体大小在Nginx配置中加client_max_body_size 10m再重启服务时间字段差8小时数据库连接时区设置不对JDBC连接URL中设置serverTimezoneAsia/Shanghai同时确认MySQL服务端时区为东八区5.2 前后端联调的高频报错分析前后端分离项目最大的拦路虎就是联调阶段的接口不通。我遇到过的最典型的情况是前端说请求400后端说没收到请求。第一个高频原因是POST请求的Content-Type不对。前端Axios默认发送JSON格式数据Content-Type是application/json而后端Controller方法用表单接收数据时JSON格式反而解析不了。解决方案是统一规范后端接口统一设计成接收JSON对象方法参数上加RequestBody注解前端Axios请求统一用JSON格式。只要两边都遵守这个约定常见的data传参坑就能完全避开。第二个高频原因是路径参数传递错位。例如后端接口是GetMapping(/customer/{id})前端试图用/customer?id1这种查询参数的形式访问结果必然404。排查接口问题时先打开浏览器开发者工具看Network面板找到实际请求的URL和后端日志里收到的请求比对一眼就能看出差别。先把URL对齐了再看参数和响应格式这是排查前后端接口问题最可靠的三步走思路。第三个隐蔽问题是数据库字段映射不对。MySQL的字段名是customer_nameJava实体类字段是customerName如果MyBatis没有开启map-underscore-to-camel-case配置实体类字段就是null前端展示出来就是个空列排查半天都不知道问题出在哪。这个配置项一行就能解决没加配置的话一天也找不出原因。5.3 性能优化与踩坑经验开发完第一版之后我感觉查询速度还行但有几个细节可以优化。第一MySQL慢查询日志要开起来。开发阶段可能看不出性能差异数据量大了以后SQL是不是慢得离谱一看日志就知道。MySQL的slow_query_log配置开启后执行时间超过阈值比如1秒的SQL都会被记录下来拿这些SQL做EXPLAIN分析加合适的索引系统的吞吐能力立刻就不一样。客户查询接口我用得最多的就是customer_name模糊查询给这个字段加了普通索引查询速度立刻提升一个量级。第二MyBatis的日志打印开关建议在开发环境开启生产环境关闭。日志打印SQL是排查问题最直接的手段但生产环境开启会拖慢系统并把大量SQL打满磁盘。我在application-prod.yml文件里把log-impl配置去掉生产环境的日志干净很多。第三列表查询接口的N1问题要提前防范。比如查询跟进记录时最开始我是在循环里逐个查客户名称客户有100条记录就多出100次数据库查询。优化方案是改用JOIN联查一条SQL把所有关联表的数据全部查出来响应时间从几秒降到了几十毫秒。这种性能瓶颈在数据量小的时候感觉不到但数据量上来了就是灾难写SQL的初始阶段就要有联查意识。第四Druid连接池的参数要和业务量匹配。默认配置的initial-size是5如果并发上来了连接数不够就会排队等着获取连接。我把min-idle设置成5、max-active设置成20同时在Druid监控页面观察连接池的使用情况再根据实际流量调整参数值。连接池太小系统在高峰期顶不住连接池太大空闲连接又浪费数据库资源一定要压测完再定最终数值。还有一个实打实的经验测试环境尽量和生产环境保持相同的数据库版本和配置。我在开发环境用MySQL 8.0结果有一同事用5.7测试JSON字段和窗口函数直接不支持浪费了一天时间排查兼容性问题。工具的版本差异看着不大落地的时候全是坑。5.4 事务边界设计与异常处理CRM系统核心业务操作几乎都涉及多表数据变更事务处理的颗粒度决定了系统的数据一致性这块儿的经验教训就多了。最值得说的是新增跟进记录这个操作。最初版本我是先insert跟进记录再update客户状态两边都是独立的SQL操作。有一次其中一条SQL执行报错客户状态没更新跟进记录却写进去了数据就不一致了。后来加上Transactional注解事务内任意一步抛错都会触发全量回滚。但要记住一个前提Transactional只对非受检异常生效。我自定义的BizException如果继承的是Exception类Spring默认不会回滚事务必须用RuntimeException做基类或者给注解指定rollbackFor Exception.class参数。事务边界的经验另一方面服务于性能。不是所有方法都适合加事务大事务会长时间占用数据库连接削弱系统的并发能力。我的处理原则是查询操作不加事务单表插入不加事务只有跨多张表写操作的方法才加。事务控制在Service层Controller层保持事务不可见性这是经典的分层设计规范。异常处理也不要散落在各处。我写了一个全局异常处理器用RestControllerAdvice注解拦截各个层抛出的异常业务异常统一返回code 400和对应的错误信息系统异常统一返回code 500并记录日志前端只需要根据code做判断。这样后端每个Controller的方法体里都不需要写try-catch代码瞬间清爽很多。全局异常拦截的好处还有一层业务异常提示语直白准确前端直接展示给用户看客户的真实使用体验也好。6. 这套系统的扩展空间与个人心得整套系统跑起来之后我又断断续续加了几个功能这些扩展思路你后面可以参考。权限控制方面目前系统是单角色管理所有用户登录后的操作权限都一样。如果销售团队比较大可以引入Spring Security或者Sa-Token加上基于角色的访问控制模型管理员、销售、市场不同角色分配不同的菜单权限和数据范围。customer表已经预留了owner_id字段做数据权限时过滤条件直接拼到SQL语句里就行。文件管理方面客户管理中经常需要上传合同附件、报价单、产品资料可以引入MinIO对象存储服务前端上传文件后拿到URL存到数据库。MinIO的部署成本极低却能极大提升系统的实用性。消息提醒方面跟进计划到了时间没人执行是销售团队管理的常见痛点。可以加一个定时任务用SpringBoot自带的Scheduled注解每天早上扫描当天的跟进计划发送邮件或者企业微信机器人通知对应的销售员。这个功能不用改动现有结构独立加一个任务模块就行。移动端适配方面销售外出见客户时经常需要用手机查客户信息当前PC端的Element Plus布局在手机上很难用。可以单独做一个小程序端或移动H5端只保留客户查询和跟进记录录入这两个高频功能复杂度可控但投入的精力不算小。在做这套系统过程中我最深的体会是前后端分离项目真正的开发难点不在任何一个单一技术点上而是跨技术栈的数据流对接能力和围绕业务的工程化思维。用Vue3写前端、SpringBoot写后端、MyBatis操作MySQL每一步单看都有教程但要把它们丝滑地串起来需要自己动手踩坑和调试。如果你也想复刻一套类似的系统我的建议是从数据库设计开始先把表结构理顺了再写后端接口最后才能安安心心做前端页面。很多新手喜欢先写页面写到一半发现接口对不上又回头改后端反反复复效率极低。自顶向下、分层推进的路子才是前后端分离项目正确的打开方式。源码里我把每一层的核心配置和注释都写到位了不管是照着敲一遍还是直接拿来做二次开发都能帮你省掉不少自己琢磨的时间。
返回列表