Spring Boot + MySQL 构建 CRM 系统全流程实践:从技术选型到部署

发布时间:2026/10/23 3:29:21
Spring Boot + MySQL 构建 CRM 系统全流程实践:从技术选型到部署 1. 为什么用这套技术栈做CRM做客户关系管理系统第一反应往往是“用成熟框架”还是“自己从零搭建”。我在实际开发中接过不少企业内部管理系统这类系统的特点很明确业务逻辑不算特别复杂但涉及的角色、客户数据、跟进记录、统计报表非常多而且需求经常变。用重型微服务架构反而累赘单体应用加上清晰的分层完全够用。选型时我定下来的技术栈是 Spring Boot MySQL Freemarker Layui ECharts Maven。这里面有几个核心判断。Spring Boot 让配置和部署变得简单不用像老式 SSM 那样写一堆 XML内置 Tomcat 直接打 jar 跑。MySQL 是客户数据存储的稳妥选择事务和支持都不差小团队也养得起。Freemarker 作为服务端模板引擎配合 Spring Boot 非常顺手数据渲染在服务端完成首屏响应快适合后台管理系统这种不需要前后端彻底分离的场景。Layui 是经典的后台 UI 框架组件丰富表格、弹层、表单都封装好了用起来比手写 Bootstrap 定制更高效。ECharts 是图表库的常青树做客户统计、销售漏斗、趋势分析时表现力很强文档也友好。Maven 负责依赖管理和打包整个项目可复现换一台电脑也能一键构建。这套组合的另一个优势是技术门槛相对亲民新人上手快。团队里如果有刚转 Java 的同事Spring Boot Freemarker Layui 的学习路径比 Spring Cloud Vue 的前后端分离要平缓得多。而且对于 CRM 这种后台系统不需要复杂的 CORS、Token 刷新、权限网关服务端 Session 拦截器就足够反而更稳定。1.1 选型时避开的几个“坑”先说为什么不选前后端完全分离。很多项目一上来就是 Vue Spring Boot 接口看着很现代但 CRM 这类系统最大的问题是权限和页面路由耦合度高如果分开写前端要维护路由守卫后端要维护接口权限两边对不上就会出漏洞。Freemarker Layui 的架构里页面模板由服务端控制菜单根据当前用户角色动态渲染权限集中在后端拦截器控制逻辑清晰也容易审计。再说不选 MyBatis Generator 反向生成全部代码。生成器能省一部分时间但 CRM 的查询条件动态变化生成的 Mapper 可读性差后期维护是灾难。我只用 MyBatis 手写重点 SQL配合简单的通用 Mapper 工具既灵活又能控制质量。还有一个细节是 JDK 版本。当时我选的是 JDK 8不是因为新版本不好而是 Spring Boot 2.x 在 JDK 8 上最稳网上资料也多遇到问题好查。如果你用 JDK 17 或 21要注意 Spring Boot 版本和依赖的兼容性否则启动时会踩到一堆模块化访问的错。2. 核心模块与数据库设计CRM 系统的核心不在技术而在数据模型。客户信息、跟进记录、商机阶段、合同订单、员工归属、沟通历史这些表结构如果没设计好后面做统计报表的时候会非常痛苦。我设计的时候坚持一条原则把“客户”作为中心所有业务都围绕客户关联但中间通过“联系人”“商机”解耦避免出现大宽表。2.1 主要功能模块拆解我这次开发的功能模块分为六块客户管理、联系人管理、跟进记录、商机管理、合同管理、数据看板。客户管理是基础支持批量导入、标签分类、状态标记比如“潜在客户”“意向客户”“成交客户”。联系人管理则挂在客户下一个客户可以对应多个联系人这样打电话、发邮件时不会找错人。跟进记录是 CRM 的灵魂每次沟通、拜访、微信聊天都要留痕时间、方式、内容、下一步计划缺一不可。商机管理用来跟踪潜在销售机会从初步接触到报价再到成交每个阶段都有预计金额和成功概率。合同管理则记录最终成交的订单回款状态也要同步更新方便财务对账。数据看板集中展示客户总数、本周新增、跟进次数、商机阶段分布用 ECharts 画成折线图和漏斗图老板打开一眼就能看清全局。权限设计上我把角色分成三类管理员、销售经理、销售员。销售员只能看自己的客户和跟进记录销售经理可以看团队的数据管理员看全部。这个权限模型不需要引入复杂的权限框架用一个角色字段加一个数据范围字段就能实现。2.2 数据表设计的关键点客户表我命名为customer核心字段包括id、name、level客户级别、industry所属行业、source来源渠道、status客户状态、owner_id归属人、create_time、update_time。这里有个容易被忽略的点owner_id一定要加索引因为销售员查询自己的客户列表是最频繁的操作。另外name字段别设成唯一索引因为现实里存在同名客户但可以加一个phone字段做唯一索引避免重复录入。跟进表follow_up字段id、customer_id、content跟进内容、next_plan下一步计划、type跟进方式、create_time、creator_id。这个表的数据量会增长很快建议建表时就考虑分区或定期归档我一般在create_time上建索引并预留一个is_deleted字段做逻辑删除防止误删数据导致关联记录断裂。商机表business_opportunity字段id、customer_id、title、amount预计金额、stage阶段、probability成功概率、expected_deal_date预计成交日期、owner_id。阶段字段我用数字枚举1 表示初次接触2 表示需求确认3 表示方案报价4 表示商务谈判5 表示成交。这样在画漏斗图时可以直接按阶段分组统计。我还会额外建一张operation_log表记录谁在什么时间做了什么操作。CRM 属于多人协作系统出了问题可以追溯。这个表不需要太复杂字段就是id、user_id、action、target_type、target_id、detail、create_time用异步方式写入不阻塞主流程。3. 实操从0搭建项目骨架与关键实现接下来是大家最关心的部分具体怎么把项目跑起来。我会从 Maven 项目结构、配置文件、登录认证、客户列表到数据看板一步步说明。为了避免篇幅膨胀我只讲关键代码和思路完整代码建议你搭框架时参考。3.1 Maven 项目结构与依赖项目名可以叫crm-system。标准的 Spring Boot 分包结构如下com.example.crm ├── controller ├── service ├── dao ├── entity ├── config ├── interceptor └── utilpom.xml里必须引入的关键依赖有spring-boot-starter-web、spring-boot-starter-freemarker、mybatis-spring-boot-starter、mysql-connector-java、lombok、druid或 HikariCP、spring-boot-starter-validation。ECharts 和 Layui 不需要通过 Maven 引入直接放到static/js和static/lib目录下用静态资源方式访问就行。版本上我建议统一用 Spring Boot 2.7.xMyBatis 用 2.3.xDruid 用 1.2.x。如果你用较新的版本注意 MyBatis 和 Spring Boot 的整合包要匹配否则会出现Invalid bound statement之类的错误。3.2 配置文件与数据源application.yml里需要配置数据源、Freemarker、MyBatis 和日志。我的配置模板如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/crm_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword type: com.alibaba.druid.pool.DruidDataSource freemarker: template-loader-path: classpath:/templates/ suffix: .ftl cache: false charset: UTF-8 mybatis: mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.example.crm.entity configuration: map-underscore-to-camel-case: true这里特别提醒serverTimezoneAsia/Shanghai一定要加否则 MySQL 驱动连接时会报时区异常。另外map-underscore-to-camel-case设置为true后数据库的create_time能自动映射到createTime字段省去一堆resultMap手写映射。3.3 登录认证与拦截器登录的核心是 Session。用户登录成功后把用户对象放进 Session同时记录用户角色和数据范围。我写了一个LoginInterceptor继承HandlerInterceptorAdapter在preHandle里判断 Session 是否有用户没有就重定向到登录页。动态菜单是 CRM 里比较有趣的部分。因为菜单项和用户角色相关我在数据库里维护了一张menu表通过角色表关联。用户登录后后端查出权限范围内的菜单列表渲染到 Freemarker 的导航栏上。这样不同角色看到的功能不同权限边界在服务端就锁死了。密码处理上我建议不要存明文而是用 BCrypt 加密。Spring Security 的BCryptPasswordEncoder可以单独引入也可以用jBCrypt工具类。登录时把用户输入的密码加密后和库里比对哪怕数据库泄露也不至于明文暴露。3.4 客户列表查询与分页客户列表是访问量最大的接口查询条件可能有客户名称、行业、状态、归属人、时间范围。这些条件是可选的所以 SQL 要动态拼。我在 Mapper XML 里用where标签和if标签处理select idselectCustomerList resultTypecom.example.crm.entity.Customer SELECT * FROM customer where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /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 /select分页用了 PageHelper 插件一行代码就能搞定PageHelper.startPage(pageNum, pageSize); ListCustomer list customerMapper.selectCustomerList(query); PageInfoCustomer pageInfo new PageInfo(list);这里有个坑PageHelper 的startPage必须紧跟 Mapper 方法调用中间不能有其他操作否则分页参数会被下一个查询消费。我因为这个 bug 排查了很久页面数据时而不分页后来加了判断才稳定。前端表格用 Layui 的table.render绑定接口返回的 JSON 格式。Layui 表格要求的 JSON 结构是{code:0, msg:, count:100, data:[]}所以后端统一返回一个Result对象包含这几个字段。这样表格组件直接解析不需要前端做额外转换。4. 权限设计与数据可视化实战这一章我要重点说说权限控制的实现细节以及 ECharts 在 Freemarker 模板里怎么用最舒服。4.1 数据权限怎么控制很多系统只有菜单权限却忽略了数据权限。我遇到过一种情况销售员登录后在客户列表里能看到所有客户只是不能点击详情。这样既不符合业务也容易泄露数据。正确的做法是在查询 SQL 里强制拼接数据范围条件。我的方案是登录时把用户的角色标识和部门标识放到 Session查询客户时在 Service 层判断角色。如果是销售员则在查询条件对象上强制设置ownerId为当前用户 ID如果是销售经理则设置deptId为当前部门 ID如果是管理员则不设置条件查询全部。这样即使前端伪造请求后端也不会返回超过权限范围的数据。在页面按钮层我还会用 Freemarker 的#if判断是否显示“编辑”“删除”按钮。比如#if sessionUser.roleId 1 button classlayui-btn idaddBtn新增客户/button /#if这样普通销售员看不到新增按钮操作入口就被藏起来了。注意这是体验层面的隐藏真正的安全还是靠后端接口拦截两处都要做。4.2 ECharts 数据看板的实现数据看板放在首页我选了四张图客户趋势折线图近30天新增客户、商机阶段漏斗图、行业分布饼图、员工客户数排行条形图。所有图表数据由一个/dashboard/data接口统一返回JSON 结构是{ customerTrend: [{date:2025-01-01,count:3}], opportunityFunnel: [{stage:初次接触,value:100},{stage:需求确认,value:80}], industryPie: [{name:制造业,value:20},{name:零售业,value:15}], ownerRank: [{name:张伟,value:30},{name:李娜,value:25}] }前端页面上我先用 Freemarker 继承公共布局再编写 JavaScript 初始化 ECharts。关键是初始化时机如果图表容器还没渲染完成就调用echarts.init会拿不到宽高导致图表空白。我在layui.use([layer], function(){})的回调里执行或者用$(document).ready确保页面 DOM 结构就绪。还有一点ECharts 的图例和提示框中文默认是正常的但如果你要自定义 tooltip比如显示“预计金额多少万”需要在配置里写formatter函数。ECharts 支持一个 HTML 片段作为 tooltip我用它来展示明细和操作链接点图表能直接跳到客户列表的筛选结果这是很多教程没讲到的实用技巧。4.3 导出与打印报表CRM 里导出 Excel 是高频需求。我用 Apache POI 生成.xlsx后端接收查询条件查出数据后用 POI 写入工作簿再把文件流写回响应。这里要注意响应头设置response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamecustomer_export.xlsx);导出文件名字如果是中文要做 URL 编码否则浏览器会乱码。另外大数据量导出建议用 SXSSFWorkbook流式 API不然几万行数据会把内存撑爆。我一开始用的 XSSFWorkbook导出五万条数据时频繁 GC换成 SXSSFWorkbook 后问题解决内存占用降了一大截。5. 部署上线与常见问题排查项目开发完最后一步是打包部署。这一章我分享一下实际部署中踩过的坑和排查方法这些问题每个人都会遇到。5.1 打包与启动用 Maven 打包前先确认pom.xml里包含了 Spring Boot Maven 插件然后执行mvn clean package -DskipTests打包完成后target 目录下会生成crm-system.jar。上传到服务器后用命令启动nohup java -jar crm-system.jar --spring.profiles.activeprod crm.log 21 如果配置了多环境在application-prod.yml里改写数据库地址和密码。这里建议把密码通过环境变量注入不要硬编码到配置文件。比如password: ${MYSQL_PASSWORD:defaultpass}启动后访问http://服务器IP:8080如果打不开先检查防火墙有没有开放 8080 端口再用curl http://localhost:8080验证应用本身是否正常。我一般还会看一眼日志确认 Tomcat 启动时间和是否有异常堆栈。5.2 时区与中文乱码系统部署到 Linux 后经常出现两个怪问题一是时间差了 8 小时二是导出 Excel 中文乱码。时间问题除了在连接串上加serverTimezoneAsia/Shanghai还要检查系统时区。可以用命令timedatectl set-timezone Asia/Shanghai另外 JVM 启动时最好加参数-Duser.timezoneAsia/Shanghai这样在代码里new Date()拿到的也是东八区时间。乱码问题多半是编码不对。MySQL 数据库表要统一用utf8mb4连接串加上characterEncodingutf8Freemarker 模板本身也要保存为 UTF-8 格式。如果还是不显示中文重启服务后看页面源码检查 meta 标签是否声明了 charset。5.3 常见问题速查表现象可能原因解决思路启动报数据库连接失败数据库地址/密码错或 MySQL 服务未启动检查application.yml用命令行连接测试页面全白无内容Freemarker 模板路径或模板语法错误看日志中的异常信息检查.ftl文件位置Layui 表格加载不出数据接口返回格式不对在浏览器控制台查看网络响应确认是标准 Result JSONECharts 图不显示容器未初始化或数据为空检查容器宽度确认init时机确认接口数据有值导出 Excel 报内存溢出使用了 XSSFWorkbook换成 SXSSFWorkbook并设置最大行数修改数据后列表不刷新浏览器缓存旧 JS给静态资源 URL 加版本参数或 CtrlF5 强刷Session 失效频繁服务器时间不准或 Cookie 过期时间太短调整server.servlet.session.timeout校准服务器时间5.4 部署时的备份与回滚上线前我给团队定了一条规矩每次发版前先备份数据库。命令很简单mysqldump -u root -p crm_db crm_db_backup_$(date %Y%m%d).sql这样万一新版本引入的 SQL 脚本有问题可以快速恢复。代码层面jar 包我保留最近三版命名带上版本号例如crm-system-1.2.jar、crm-system-1.3.jar。切换版本时只需要停掉旧进程启动新进程如果十分钟内出现严重问题马上切回旧包回滚时间控制在五分钟内。很多人不重视这个流程等出了事故才手忙脚乱其实提前做好很便宜。6. 项目复盘与扩展建议写到这里整套系统基本成型。最后我想聊聊我实际开发完之后的复盘以及如果继续往下做会优先扩展哪几个方向。6.1 这套架构的最优适应场景如果你现在负责的是几十人规模的销售团队客户管理这个系统完全够用。我有一次把它部署到某家公司销售员日常记录跟进、查询客户销售经理看团队业绩图表管理员管理用户和合同运行大半年没出过性能问题。因为单表数据量不超过百万MySQL 加索引扛得住后端单体应用也不需要横向扩展。但如果客户量增长到百万级并且要对接审批流、ERP、财务系统那我会考虑把系统拆分成客户中心、商机中心、合同中心三个微服务用消息队列异步处理同步需求。不过在动手拆分前先要想清楚周报、月报、统计报表的数据怎么聚合微服务最忌讳的是拆了之后数据变成孤岛。6.2 可以继续扩展的功能热点从业务价值角度看有三个方向值得优先做。第一是客户公海池。当销售员长期不跟进某个客户时系统自动把客户退回公海其他销售员可以认领。这个功能只需要加一个update_time判断和定时任务但能显著提升客户周转率老板非常喜欢。第二是跟进提醒。利用 Spring Boot 的定时任务每天早上扫描当天需要跟进的客户给对应销售员发邮件或者站内消息。这个功能实现不复杂但用户体验提升非常明显不然销售员总是忘记上次说的“下周再联系”到底是哪天。第三是移动端适配。现在的后台页面是针对 PC 的销售外出用手机打开时表格挤成一团。如果要做我建议单独做一套 H5 页面接口复用现有的后端只换视图层工作量可控。Layui 官方也有移动端方案不过不如换个轻量 UI 来得干净。6.3 我的几点开发心得最后分享三个我踩过坑之后形成的习惯。第一写 SQL 时永远注明预期扫描行数。简单来说每一条查询都要问自己这个查询会走索引吗如果数据量翻十倍性能还扛得住吗客户表最常用的字段一定加索引但不要盲目给每个字段加索引太多反而拖慢写入速度。第二日志要按级别区分。业务操作日志和系统运行日志分开不要全打印到控制台。我给每个接口写了一个简单的Log注解自动记录请求路径、耗时和操作人。排查问题时非常有用能直接看到某个用户某天做了哪些操作。第三不要过早优化。这套项目一开始没有引入 Redis连缓存都没有后来客户量上来了才发现列表页有点慢才加了 Redis 缓存热点数据。前期最重要的还是业务跑通数据库字段设计合理后期加缓存是很自然的事。如果你一开始就用 Redis代码复杂度高了反而不利于新人接手。这套 CRM 系统做完之后后续维护的成本其实很低。技术选型稳、数据模型清晰、权限边界明确这就是这类管理系统最重要的三个点。希望这篇文章能给你一把不错的钥匙照着搭一套自己的客户管理系统把业务流程管起来也让团队协作顺畅一些。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询