ARTICLE DETAIL

资讯详情

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

Java SpringBoot CRM客户关系管理系统源码实战:从表设计到部署

Java SpringBoot CRM客户关系管理系统源码实战:从表设计到部署 简介客户关系管理系统是企业数字化的核心业务场景之一它通过管客户、管跟进、管结果三个维度实现销售过程的透明化与可追溯。不同于高并发电商或复杂IMCRM更考验业务链路的完整性与数据权限的精细化设计。基于SpringBoot构建单体CRM可快速实现客户管理、公海池、跟进记录、商机合同等核心模块满足中小企业内部管理与毕业设计、跳槽项目包装等实际需求。同时借助MyBatis-Plus动态查询与Sa-Token权限控制能有效平衡开发效率与业务复杂度。从数据库表结构设计、索引优化到原子SQL防并发领取这套源码沉淀了完整的工程化实践。无论是准备技术面试还是落地一套可维护的业务系统掌握该项目的设计思路和部署经验都能在真实业务中发挥重要价值。 做Java开发这么多年我一直觉得CRM系统是每个后端程序员绕不开的练手项目同时也是一个实战价值很高的业务系统。它不像电商那样强调高并发也不像IM那样复杂但胜在业务链路完整客户、联系人、商机、跟进、合同、数据统计这些模块串起来之后基本就覆盖了企业中后台系统的常见形态。市面上开源的CRM不少比如悟空CRM、芋道源码这类但大多比较重。如果你是准备毕业设计、跳槽项目经验包装或者公司内部需要一个轻量CRM用SpringBoot从零搭一个可控性最高的系统其实是最务实的选择。这篇就围绕“Java SpringBoot CRM客户关系管理系统源码”这个主题把我在实际开发这类系统时的设计思路、技术选型、核心功能实现、源码结构和部署经验完整拆一遍。我不会只贴一堆代码跑一个CRUD就完事而是会把每一步“为什么这么做”讲清楚包括表结构怎么设计、数据权限怎么做、跟进记录怎么防重复、统计数据怎么预聚合这些容易踩坑的点。适合刚学完SpringBoot想做项目练手的同学也适合准备从单体往业务系统扩展的初中级开发者参考。1. 整体设计思路与核心需求拆解1.1 CRM系统到底在解决什么问题先想清楚一件事CRMCustomer Relationship Management客户关系管理系统不是简单的“记录客户电话和姓名”的通讯录它要解决的核心问题是销售过程的透明化和可追溯。公司老板想知道每个销售手里有多少客户、哪些客户快成交了、跟进到什么阶段了销售自己想知道下一个该跟进谁、上次聊了什么、这个月业绩还差多少。这些需求归纳下来就是三个核心动作管客户、管跟进、管结果。从“管客户”出发系统要有客户信息维护、公海池客户回收和分配、客户查重、客户标签从“管跟进”出发系统要有跟进记录、跟进计划、下次跟进提醒从“管结果”出发系统要有商机阶段管理、合同订单、回款记录和销售数据看板。把这几个模块做扎实一个CRM的主体功能就立住了剩下的比如产品管理、工作台提醒、操作日志都是在这个骨架上的增量。我见过不少同学拿开源项目改完就当自己的项目结果面试官一问“公海池回收规则怎么设计的”就卡壳。所以这篇文章里我会把设计逻辑讲透你就算不照着写代码把思路吃透了也是个加分的项目亮点。1.2 技术选型为什么是SpringBoot而不是Spring Cloud技术选型这件事最怕的就是过度设计。一个面向销售团队使用的CRM系统用户量撑死几百上千人根本不需要微服务那一套。选SpringBoot单体架构理由很实在开发效率高Spring Boot的自动配置和Starter机制把大量模板配置省略掉了一个spring-boot-starter-web就能起服务不需要像SSM时代那样写一堆XML。部署运维简单打成jar包扔到服务器上java -jar就完事不像微服务要管注册中心、配置中心、网关运维成本直线下降。事务好控制下单、分配客户、更新商机阶段这些操作涉及多张表的数据变更单体架构直接在一个Service方法上加Transactional就能保证一致性拆成微服务反而要在分布式事务上折腾半天。团队协作适中按controller、service、mapper分层三个人开发一个CRM系统代码冲突都很少。持久层框架我选MyBatis-Plus这个不是情怀是因为CRM这种业务系统里动态条件查询特别多。客户列表要根据名称、来源、状态、创建时间、负责人多个条件组合筛选用MyBatis-Plus的LambdaQueryWrapper写起来比手写XML拼SQL要爽太多了。不过复杂的报表统计我还是倾向用XML手写SQL因为group by加多表联查的SQL用Wrapper构造出来可读性很差而且不好调优。认证授权方案用的是Sa-Token不是Spring Security。虽然Spring Security功能强大但它的配置复杂度对这个体量的系统来说有点重了。Sa-Token开箱即用登录、踢人下线、权限校验注解都有几百行代码就能集成完。当然这个可以根据个人偏好替换JWT拦截器也是经典方案后面我会讲一下JWT方案的注意事项。1.3 项目模块规划与包结构设计拿到项目第一件事不要急着建Spring Initializr先规划目录结构。我是按业务模块划分包而不是按技术层次划分包。原因很简单如果你先建controller、service、mapper这种顶层包然后再往里塞各业务模块的类业务一多就会很乱比如com.example.crm.controller.CustomerController和com.example.crm.service.CustomerService之间跳来跳去找代码全靠全局搜索。按业务模块划分以后一个模块的所有类都在一个包下开发时只需要在这个包内操作高内聚低耦合。我推荐的结构是com.company.crm作为根包下面再分modules子包每个业务模块单独成一个包com.company.crm ├── common # 通用组件统一返回体、异常处理、工具类、配置类 ├── framework # 框架配置Sa-Token配置、MyBatis-Plus配置、CORS配置 ├── modules │ ├── customer # 客户模块 │ │ ├── controller │ │ ├── service │ │ ├── mapper │ │ └── entity │ ├── contact # 联系人模块 │ ├── track # 跟进记录模块 │ ├── business # 商机模块 │ ├── contract # 合同模块 │ └── system # 系统管理模块用户、角色、菜单、部门 └── AuthApplication.java # 启动类common和framework的区别在于common是业务无关的通用类比如ResultT返回体、BusinessExceptionframework是框架层面的配置类。这样分清楚以后后面接手的同事看目录结构就能明白哪些是基础设施、哪些是业务代码。我踩过的坑就是一开始把配置类全部扔到config包里后来配置越来越多找起来极其痛苦。2. 数据库设计CRM系统的地基怎么打2.1 核心表结构一览与设计思路数据库设计是整个系统最核心的部分表设计一旦定型后面的业务逻辑全是围绕它转的。CRM系统的核心表我用这八张来覆盖表名说明核心字段sys_user用户表id、username、password、real_name、dept_id、statussys_role角色表id、role_name、role_code、data_scopesys_user_role用户角色关联表user_id、role_idcrm_customer客户表id、name、phone、source、level、owner_id、status、follow_status、next_timecrm_contact联系人表id、customer_id、name、phone、position、is_primarycrm_track_record跟进记录表id、customer_id、contact_id、track_type、content、next_time、creator_idcrm_business商机表id、customer_id、name、amount、stage、expect_trade_date、owner_idcrm_contract合同表id、contract_no、customer_id、business_id、amount、sign_date、status2.2 客户表状态字段的设计细节客户表是CRM的“心脏”它的字段设计直接影响后续所有功能。我建议客户表至少包含status和follow_status两个状态字段但它们的含义完全不同。status是客户生命周期状态一般包括潜在客户、有效客户、成交客户、流失客户follow_status是跟进状态表示“这款客户当前是否正在被跟进”主要用于“待跟进提醒”功能比如客户设置了下次跟进时间到了时间系统要推送给销售。这两个状态千万别合二为一否则统计“本月新增了多少潜在客户”和“有多少客户该跟进了”会互相干扰。比如一个成交客户可能仍然需要售后跟进如果status直接覆盖了follow_status售后跟进计划就丢了。还有字段类型的取舍问题。客户级别、来源这类字段我强烈建议用varchar存储字典编码比如level字段存A、B、C不要直接存中文“重要客户”或“普通客户”。原因有两个一是数据库层面排序不友好按级别排序时需要自定义排序规则二是以后字典项改名时要写大量update语句。查询的时候再用Trans或者字典翻译逻辑转成中文展示。我在实际开发中遇到过直接存“高”“中”“低”的项目后来产品说要改成“S-A-B-C”等级光改数据就花了半天这就是设计时偷懒的代价。2.3 公海池与客户分配的设计要点公海池是CRM区别于普通管理系统的标志性功能通俗讲就是“无主客户资源池”。客户没有负责人或者因为长期未跟进被系统自动回收就会掉入公海池其他销售可以捞取。怎么设计客户表里的owner_id字段就是关键。owner_id为null或者为0就代表客户在公海池。分配客户就是更新owner_id回收客户就是将owner_id置空。这里有个重要的细节回收操作不能简单置空要额外记录回收原因和回收时间。我建议加一个crm_customer_recycle_log表每次回收都写一条日志客户ID、操作人、回收原因、回收时间、流入公海时间。这样既能追溯客户历史归属又能为以后做“回收率统计”提供数据。公海池回收规则一般是客户被分配后超过N天未跟进根据follow_status和last_track_time判断就自动流回公海。这个定时任务用Spring的Scheduled注解实现就行每天凌晨跑一次。规则参数比如N天要设计成系统配置项不要写死在代码里不然产品上线以后想调规则还得改代码重新发布。2.4 跟进记录为什么单独建表很多人会觉得跟进记录就是客户表里的一个备注字段最多加个时间戳这种想法在系统初期还能凑合一旦客户量上来就废了。一个客户可能一个月被跟进十几次每次跟进的类型电话、拜访、微信和内容都不同如果全部存在一个字段里数据被覆盖、查询困难、统计无从谈起。跟进记录单独建表核心价值是每次跟进在客户详情页有完整的时间线展示谁在什么时候跟进了什么内容一目了然。可以统计分析每个销售的跟进频率、跟进方式占比评估销售执行力。为“上次跟进时间”“下次跟进时间”这类计算字段提供数据基础。跟进记录表的设计上有一个隐藏难点客户和联系人之间的关系。一次跟进可能对应一个客户下的某个联系人所以表里既要有customer_id又要有contact_idcontact_id允许为空因为有些跟进场景可能没对接具体联系人。2.5 索引设计的实际经验很多新手设计表的时候不加索引数据量小没问题等客户量到几万条、跟进记录到几十万条的时候列表页一个order by排序就可能慢到超时。CRM系统里最常用的查询就是“当前登录用户负责的客户列表”所以crm_customer表的(owner_id, status)联合索引必须有。其次跟进记录表经常按客户查询crm_track_record表的customer_id索引必须有。再就是客户查重功能会按手机号或客户名称精确查询单独索引足够不需要联合索引。还有一个小技巧客户名称上尽量用前缀索引比如KEY idx_customer_name (name(20))这样能显著减少索引体积。客户名称一般不像电话号码那样必须精准匹配模糊查询可以交给LIKE keyword%走前缀索引效率很高不要一上来就是全文索引CRM这种数据量级用不上。3. 后端核心功能实现与踩坑实录3.1 统一返回体与全局异常处理写接口第一步先定返回体规范不然后面每个接口返回结构五花八门前端对接成本极高。我通常定义一个ResultT泛型类包含code200成功/500失败、message、data三个字段所有Controller接口都返回这个结构。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(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }与之配套的是全局异常处理器用RestControllerAdvice统一拦截业务异常和系统异常。业务异常比如客户名已存在、客户已被回收统一定义为BusinessException在处理器里返回code500把业务提示消息返回给前端。系统异常NullPointerException之类则记录详细堆栈返回一个友好提示“系统繁忙请稍后重试”避免把敏感堆栈信息暴露给前端。3.2 登录认证与权限控制的落地细节登录认证用Sa-Token实现的话代码结构很清晰PostMapping(/login) public ResultString login(RequestBody LoginDTO loginDTO) { // 1. 校验用户名密码 LambdaQueryWrapperSysUser wrapper new LambdaQueryWrapper(); wrapper.eq(SysUser::getUsername, loginDTO.getUsername()); SysUser user sysUserMapper.selectOne(wrapper); if (user null || !MD5Util.verify(loginDTO.getPassword(), user.getPassword())) { throw new BusinessException(用户名或密码错误); } // 2. 校验用户状态 if (user.getStatus() ! 1) { throw new BusinessException(账号已被禁用请联系管理员); } // 3. 登录并返回token StpUtil.login(user.getId()); return Result.success(StpUtil.getTokenValue()); }密码存储绝对不能明文MD5加盐也好、BCrypt也好BCrypt更推荐虽然计算慢一点但安全性高很多。权限控制上Sa-Token提供注解SaCheckPermission(customer:add)在Controller方法上标注即可。权限码建议按模块:操作的格式命名比如customer:add、customer:delete、track:add这样代码里一读权限码就知道是哪个模块的哪个操作。这里有个最容易忽略的坑数据权限和功能权限是两码事。功能权限控制的是“能不能点这个按钮”数据权限控制的是“能看到哪些数据”。销售只能看到自己负责的客户销售主管能看到本部门所有客户的客户老板能看到全部客户的客户这种需求在CRM里必然出现。数据权限不能靠注解硬编码我是在CustomerService的查询方法里根据当前登录用户的角色动态拼接owner_id条件实现的。具体做法是把查询条件封装到一个CustomerQueryDTO对象Service里根据角色编码判断如果是普通销售就强制在查询条件上加上owner_id 当前用户ID如果是主管就查出本部门所以用户ID列表用IN条件如果是老板就直接查全部。3.3 大客户列表的多条件动态查询实现客户列表查询是CRM里最常用的接口它的实现质量直接影响用户体验。多条件组合查询的核心是动态拼接SQL这里MyBatis-Plus优势非常明显public PageResultCustomerVO pageCustomer(CustomerQueryDTO queryDTO) { PageCrmCustomer page new Page(queryDTO.getPageNum(), queryDTO.getPageSize()); LambdaQueryWrapperCrmCustomer wrapper new LambdaQueryWrapper(); // 动态拼接条件 wrapper.like(StringUtils.hasText(queryDTO.getName()), CrmCustomer::getName, queryDTO.getName()) .eq(StringUtils.hasText(queryDTO.getPhone()), CrmCustomer::getPhone, queryDTO.getPhone()) .eq(queryDTO.getStatus() ! null, CrmCustomer::getStatus, queryDTO.getStatus()) .eq(queryDTO.getLevel() ! null, CrmCustomer::getLevel, queryDTO.getLevel()) .ge(queryDTO.getCreateStart() ! null, CrmCustomer::getCreateTime, queryDTO.getCreateStart()) .le(queryDTO.getCreateEnd() ! null, CrmCustomer::getCreateTime, queryDTO.getCreateEnd()); // 数据权限控制 applyDataScope(wrapper); // 排序规则先按跟进状态倒序未跟进的排前面再按下次跟进时间升序 wrapper.orderByAsc(CrmCustomer::getFollowStatus) .orderByAsc(CrmCustomer::getNextTime) .orderByDesc(CrmCustomer::getCreateTime); PageCrmCustomer result customerMapper.selectPage(page, wrapper); // 转换为VO翻译字典字段 return convertToPageResult(result); }这个方法里有两个值得注意的点。首先是orderByAsc(CrmCustomer::getFollowStatus)这个排序逻辑把“待跟进”的客户排在列表最前面销售每天打开系统第一眼就看到今天该干啥这个细节比功能本身还重要产品满意度提升非常明显。其次要警惕的是like查询的性能问题如果客户名称输入的是一个很短的字符串比如只输入“张”%张%会扫全表但这种场景一般出现在数据量超过十万条之后前期不用过度优化。3.4 客户查重与分配的高并发处理误区客户查重逻辑看着简单但并发场景下容易出问题。假设两个销售同时导入一批客户都在检查“手机号13800138000是否已存在”发现不存在就插入结果就插入了两条几乎一样的客户记录。解决办法是数据库层面加唯一索引比如uk_phone (phone)前提是手机号是唯一业务标识insert时数据库会报DuplicateKeyException捕获这个异常转换成友好提示“该手机号客户已存在”。客户分配时也有类似的并发问题。一个公海池客户两个销售看到后同时点击“领取”如果代码逻辑是“先查询owner_id是否为空再更新owner_id”就会产生重复分配。必须改成一条原子SQLUPDATE crm_customer SET owner_id #{newOwnerId}, assign_time NOW() WHERE id #{customerId} AND (owner_id IS NULL OR owner_id 0)这条SQL执行后判断受影响行数如果为0说明客户已经被别人领走了提示“手慢了客户已被领取”。这里不要用select for update或者加分布式锁动静太大了一条原子更新就能解决问题。3.5 跟进提醒与数据看板的实现要点跟进提醒功能我用的方案是“查询时计算”不搞复杂的任务调度推送。销售登录系统后首页工作台会显示“今日需跟进客户”列表SQL条件就是owner_id 当前用户 AND next_time NOW() AND follow_status 1。这个查询命中(owner_id, follow_status, next_time)联合索引的话性能还是可以的不用实时推送销售点击刷新就能看到产品体验完全够用。数据看板是另一个高频功能销售漏斗图、业绩排行榜、跟进统计图。这些图表数据如果每次加载都实时统计数据库压力会很大。我的实际做法是分两块实时性要求低的核心指标比如每个月的业绩汇总用定时任务每天凌晨算好写进统计数据表实时性要求相对高的动态数据比如当前登录用户今天新增了几个客户、写了几条跟进直接用count查询数据量小无所谓。这个思路在资源有限的单体项目里非常实用不用上一个OLAP列式存储就把90%的报表需求覆盖了。4. 源码工程化实战从Controller到部署4.1 前端页面与接口的对接规范虽然是SpringBoot后端项目名义但一个完整的CRM系统不可能没有页面。我这里篇幅有限不展开写Vue的全部代码但必须说清楚前后端对接规范因为接口设计是否合理直接影响协作效率。我推荐的前端技术栈是Vue3 Element Plus Axios这是目前国内中小企业后台最主流、上手最快的组合。Axios统一封装会做三件事携带token请求头、处理响应拦截、统一错误提示。具体来说每次请求在request拦截器中从localStorage取token放到Authorization头里后端Sa-Token的鉴权拦截器会自动校验response拦截器中判断code字段如果是401跳转到登录页如果是500直接ElMessage.error(message)弹出后端传回来的业务提示。接口路径规范建议采用RESTful风格比如客户资源操作方法URL说明GET/api/customer/page分页查询客户列表GET/api/customer/{id}查询客户详情POST/api/customer新增客户PUT/api/customer修改客户DELETE/api/customer/{id}删除客户POST/api/customer/receive领取公海客户所有接口前缀加/api前端代理到后端端口Nginx部署时再统一配置转发。这样前后端联调时只需要确认一套路径规范不会出现在前端写/customer/add、后端映射/crm/customer/save这种驴唇不对马嘴的情况。4.2 核心页面逻辑客户列表与详情客户列表页面的功能逻辑很典型搜索栏名称、手机号、状态、负责人等条件、表格区展示客户名称、手机号、级别、负责人、下次跟进时间等字段、分页器。列表页的技术难点其实在“操作列按钮的动态显隐”上要根据当前用户的权限码判断是否显示“编辑”“删除”“分配”按钮这个通过Vue的v-if配合后端接口返回的权限码数组实现。客户详情页会稍微复杂一点它由多个Tab组成客户基本信息、联系人列表、跟进记录时间线、商机列表、合同列表。这种页面后端要提供一个组装好的CustomerDetailVO把客户基本信息、下属联系人列表、最近跟进记录一次性返回避免前端发五个接口慢慢拼。虽然会多传一些冗余数据但整体加载速度更快前端代码更简单。4.3 MyBatis-Plus代码生成器与逆向工程手写每个模块的Entity、Mapper、Service既枯燥又容易出错。MyBatis-Plus官方提供了代码生成器可以连上数据库自动生成这些基础代码。我之前也推荐生成的代码但用多了以后我发现直接生成出来的代码有个问题生成的Service接口层是空壳Service实现类也几乎没业务逻辑Entity还是数据库表字段的一一映射。这些代码本身没毛病但它会让项目看起来特别臃肿每个模块都是五个类entity、mapper、service、serviceImpl、controller一个CRM系统十几个模块下来就是六七十个类很多类从头到尾没写过一行业务代码。所以我的建议是生成器只用来生成Entity和MapperService和Controller自己手写。Entity是数据库结构的映射生成器生成完全没问题Mapper接口的selectPage、selectOne这些通用方法已经够用。Controller和Service是业务逻辑的载体必须自己动手写哪怕是一开始没有业务逻辑也要在写的过程中把接口命名、参数校验、统一返回体的路数走通这样后续加业务才有感觉。4.4 SpringBoot项目配置与多环境处理实际开发中一台开发机、一台测试服务器、一台生产服务器数据库地址、Redis地址、日志级别大概率都不一样。把配置写在application.yml里要不得因为这意味着每次部署到不同环境前都要改配置文件改漏了就上线出事故。正确做法是配置多环境文件resources/ ├── application.yml # 公共配置 ├── application-dev.yml # 开发环境 ├── application-test.yml # 测试环境 └── application-prod.yml # 生产环境application.yml里用spring.profiles.active指定当前使用哪个环境配置。启动时通过--spring.profiles.activeprod参数覆盖。生产环境密码、密钥这类敏感信息不要直接写在application-prod.yml里提交到代码仓库用环境变量注入的方式更安全。日志配置我建议分开application.yml配置上logging.file.name然后logback-spring.xml里按天滚动保留30天。CRM这种业务系统出错时排查日志是第一手段日志级别生产环境用info就行如果遇到问题再动态调低到debug级别看详细SQL。另外spring.datasource.druid连接池配置里maxActive大小要根据业务量设置我通常给20连接池太小会出现获取连接超时太大又浪费数据库连接资源。4.5 项目部署上线与运维注意事项部署这块我直接推Linux服务器 Nginx反向代理 systemd管理Java进程。SpringBoot项目打出来的jar包有几十MB上传到服务器后用nohup java -jar跑起来唯一的坑是进程一重启就丢了管理。systemd写好crm.service文件之后systemctl start crm、systemctl restart crm、systemctl status crm都能用进程挂了自动重启比nohup强太多了。[Unit] DescriptionCRM System Afternetwork.target [Service] Userapps ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /opt/crm/crm-system.jar SuccessExitStatus143 Restartalways RestartSec10 [Install] WantedBymulti-user.targetJVM参数里的-Xms512m -Xmx1024m要根据服务器内存实际情况调整。我见过很多人在1G内存的小服务器上直接把堆内存开到1G结果系统卡死这就是没留足系统本身的内存。另外生产环境不要忘了加-Dspring.profiles.activeprod不然还是会读默认的dev配置。Nginx层面要注意两个点一是client_max_body_size要设置一下否则上传图片时超过默认1M会直接报413错误客户头像、合同附件很容易超过这个值二是WebSocket或者长时间请求的proxy_read_timeout要调大不然页面导出大量数据时容易断掉。部署完成后一定要测试一下上传、导出的功能不要等业务反馈Bug了才去翻Nginx错误日志。5. 常见问题排查与避坑经验清单5.1 数据库相关连接失败、时区报错、中文字符乱码这个算是SpringBoot项目最常见的坑了。时区问题最典型启动的时候一直报连接成功但查询CURRENT_TIMESTAMP时间差8小时大概率是JDBC连接串没加serverTimezoneAsia/Shanghai。字符集问题表现在插入中文后变成了???这时候检查三处MySQL数据库字符集、表的字符集、JDBC连接串是否指定characterEncodingutf8三处全部统一成utf8mb4才能彻底解决。utf8mb4是utf8的超集能存emoji表情和生僻字无脑选它。还有一个隐藏较深的坑MySQL的max_allowed_packet默认值是4M如果往数据库插入一条特别长的跟进记录比如粘贴了一段长文本可能会报Packet too large的错误。修改MySQL配置文件my.cnf把max_allowed_packet调到16M或更大并重启数据库服务即可。5.2 JVM内存溢出与内存崩溃的排查思路热词里出现过java: outofmemoryerror: insufficient memory这个在CRM部署到小内存服务器时很容易出现。排查分两步先看是不是JVM堆内存分配过大超出了物理内存启动时会直接报错把-Xmx调小即可比如512m的小服务器给Java进程256m或384m。再看是不是堆内存实际使用量持续上涨直到OutOfMemoryError这通常是内存泄漏比如查询数据量太大、循环里不断创建对象、静态集合没有清理等。真实运维中我发现最多的情况反而是第一种服务器的物理内存一共512M-Xmx1024m显然是错误的改成-Xmx384m后一直很稳定。这里有个个人体会内存不够的时候不要只调JVM参数还要检查有没有不必要的中间件常驻进程占内存比如一台小服务器上同时跑MySQL、Redis、Nginx和一个Java服务内存自然会紧张。5.3 登录失效与权限校验的几个常见症状登录认证这块的问题通常有三种症状。第一种登录后调用接口每次都返回401这往往是前端Axios没有正确携带token或者后端配置了StpInterface但接口路径排除时遗漏导致每次请求都走登录校验。第二种权限注解不生效SaCheckPermission标在方法上完全没有拦截效果检查是不是没有注册全局异常处理器Sa-Token抛出NotPermissionException后没被处理前端看到就是500错误或者泛白的提示。第三种用户改完角色后权限没更新Sa-Token默认缓存了用户的权限列表需要调用StpUtil.logout(userId)把会话注销后重新登录。提示修改用户角色、禁用用户账号这类操作别只改数据库字段要联动调用Sa-Token的StpUtil.logout()让用户重新登录权限才能即时生效。5.4 SpringBoot版本兼容性与依赖冲突排查现在SpringBoot版本更新很快如果你刚接触这个项目直接拉最新的大版本很可能会遇到各种依赖兼容问题。比如SpringBoot 3.x基于Jakarta命名空间老一些的MyBatis-Plus版本根本跑不起来因为javax.servlet换成了jakarta.servlet。我的建议是选择稳定组合SpringBoot 2.7.x MyBatis-Plus 3.5.x Sa-Token 1.3x MySQL 5.7/8.0。这套组合经过大量项目验证依赖冲突很少。如果真遇到了依赖版本冲突排查思路很简单找到mvn dependency:tree输出里的冲突依赖用exclusion排除掉不需要的版本或者显式指定依赖版本让Maven选择正确的传递依赖。不要盲目升级版本一个稳定的技术栈比最新的功能重要得多。5.5 常见问题速查表现象可能原因解决方向插入中文变问号数据库/表字符集不是utf8mb4修改库表字符集并重建连接时间差8小时JDBC连接串没指定时区加serverTimezoneAsia/Shanghai客户领取总是提示已被领取并发领取原子SQL没生效用UPDATE WHERE owner_id IS NULL或加锁上传图片报413Nginxclient_max_body_size太小调大该参数内存溢出JVM堆内存配置过大/内存泄漏调低-Xmx并用jstat查看GC情况权限注解不生效缺少异常处理器加上RestControllerAdvice捕获NotPermissionException查询速度越来越慢缺少索引按执行计划加联合索引6. 从源码到二次开发扩展方向与心得源码拿到手以后怎么做到既能交付给业务方用又能变成自己手里可复用的技术资产我最后分享三个扩展方向和一点个人体会。方向一增加自定义字段能力。不同行业的客户需要的字段差别很大有人要“客户规模”有人要“客户来源平台”。常规做法是在客户表预留多个扩展字段比如ext_field1到ext_field10再在数据库维护一张字段配置表让管理员在后台自由配置这些字段的显示名称和类型。这样不用改代码就能适应不同业务团队的个性化需求是CRM产品化的第一步。方向二增加数据报表模块。系统跑一段时间后老板必然会问“这个月的成交转化率怎么算”“哪个销售的拜访次数最多”。提前设计一张统计数据表每天凌晨定时按销售员、日期、客户状态变化做预聚合报表页面直接读统计数据表性能和实时性体验都会有保障。方向三增加移动端适配。销售天天在外面跑客户不可能随时开电脑。最务实的方案不是开发单独的App而是把现有的管理端页面做成响应式布局或者单独做一个移动端H5页面展示“今日待跟进客户”和“快速写跟进记录”两个核心功能。这样投入小、见效快业务方对系统的满意度会有明显提升。最后再说一点个人实际的感受。我在开发和维护这类CRM系统的过程中最大的体会是客户关系管理系统真正的难点从来不在技术而在业务理解。技术方案上SpringBoot MyBatis-Plus Sa-Token MySQL这套组合已经能把CRM系统的日常功能支撑得很好真正拉开差距的是你对“销售怎么看客户、管理者怎么看业绩”这两个问题的理解深度。多去听销售吐槽几句多看看他们日常的工作习惯再回到代码层面去设计字段和状态机你做出的系统会比其他工程师的版本有灵魂得多。本文还有配套的精品资源点击获取
返回列表