接手这类老牌技术栈组成的CRM系统,第一感觉往往是“这年头还有人用Freemarker加Layui?”但真正做完一套基于JavaWeb和MySQL的Spring Boot CRM客户关系管理系统,你会发现这套组合在中小型企业内部项目里的战斗力相当可观。后端Spring Boot负责业务逻辑,MySQL存数据,Freemarker在服务端渲染页面,Layui撑起后台管理界面,ECharts把客户数据转成图表看板,Maven统一管依赖,整套流程走下来,我对这套复古但务实的选型有了全新的认识。这篇文章就把项目里的设计与实现细节完整拆一遍,包括数据库设计、核心功能模块、实操中的排查经验,希望能给正在做同类系统或者准备做课程设计、毕业设计的读者一些参考。
1. 这个CRM系统到底做了什么:项目定位与功能拆解
1.1 标题里的每一个词,对应的是什么
先拆一下标题“基于javaweb和mysql的springbootcrm客户关系管理系统(java+springboot+echarts+freemarker+layui+maven+mysql)”,这一串关键词混在一起容易看懵,但剥开之后其实非常清晰。
从业务层面看,它是一套标准的客户关系管理系统,也就是CRM。核心场景是企业的销售、市场、客服部门用来管理客户资料、跟进记录、商机状态、合同信息,最终目标是让客户资源不再散落在每个人的聊天记录和Excel表里。从技术层面看,它就是一个典型的JavaWeb项目,后端用Spring Boot搭建,数据存在MySQL里,前端页面不是前后端分离的SPA,而是由Freemarker在服务端把HTML渲染好再交给浏览器。Layui负责页面的样式、表格、弹窗、表单验证,ECharts负责数据看板的图表展示,Maven负责项目构建和依赖管理。
这套系统能解决的问题其实非常具体:一是客户资料统一管理,销售离职不会把客户带走;二是跟进记录有迹可循,领导和同事能看到每个客户的最新进展;三是商机漏斗和销售报表可视化,管理层不用再靠人工汇总Excel看数据。对于正在做毕业设计、课程设计,或者在小团队里负责内部管理系统开发的开发者来说,这套系统的代码结构和业务逻辑都能直接复用,非常值得参考。
1.2 核心功能模块
功能模块上,整套系统我拆成了六个大块:系统登录与用户权限、客户管理、联系人管理、商机管理、跟进记录、数据看板。
登录模块不做花哨的验证码和第三方集成,就是简单的账号密码校验,配合拦截器对未登录Session进行拦截。权限部分我用的是角色控制,预设了管理员、销售主管、普通销售三种角色,菜单和操作按钮根据角色动态展示。
客户管理是核心模块,支持新增客户、修改资料、删除(这里用的是逻辑删除,后面细说)、分页查询、按名称或行业筛选、导出Excel。联系人模块挂在客户下面,一个客户可以对应多个联系人,联系人字段包含姓名、电话、邮箱、职位、是否为主要联系人。商机模块记录销售机会,状态用管道阶段表示,比如初步接触、需求确认、方案报价、谈判签约,每个阶段可以设置预计成交额和预计成交日期。跟进记录模块保存每次和客户沟通的时间、方式、内容概要。数据看板模块用ECharts展示客户行业分布饼图、每月新增客户趋势折线图、商机阶段漏斗图以及销售排行榜。
1.3 适合哪些人参考
如果你是刚学完JavaWeb基础,准备做项目练手或者交毕业设计的在校生,这套系统的技术难度曲线很平缓。Spring Boot和Freemarker的组合比前后端分离少了一大截联调工作,Layui的表格组件自带分页和重载,不用自己写复杂JavaScript。如果你是在小公司做内部系统开发的工程师,这套架构能帮你用相对少的代码交付一套能跑的业务系统,后续维护也不难。对已经习惯前后端分离的开发者来说,这篇文章更重要的是提供了一种不同的布局思路:服务端渲染在某些场景下效率真的更高。
2. 为什么选这套组合:技术选型背后的真实考量
2.1 Spring Boot与JavaWeb的关系
很多初学者容易把JavaWeb和Spring Boot当成两个对立的东西,实际上Spring Boot就是JavaWeb开发的一种现代实现方式。传统的JavaWeb项目需要部署到外部Tomcat,配置web.xml,手动引一堆Jar包,用SSH或SSM框架时XML配置铺天盖地。Spring Boot把这些繁琐的东西全部自动化了,内嵌Tomcat、自动配置数据源、starter机制管理依赖,几行代码就能跑起一个Web应用。
在这个项目里,Spring Boot用的版本是2.7系列,因为Freemarker、Shiro等老牌库对Spring Boot 3.x的Jakarta命名空间适配不太完善,用2.7最省心。Spring Boot在这里承担的工作包括:控制器层路由分发、Service层业务逻辑编排、Mapper层数据访问、拦截器注册、全局异常处理、模板引擎自动配置。
2.2 Freemarker为什么不算老古董
前后端分离的开发模式流行之后,很多人把Freemarker、Velocity这类服务端模板引擎归类为古董技术。但从实际项目角度看,企业内部管理系统选择Freemarker有几个不可替代的优势。
第一,SEO问题在后台管理系统里不存在,但首屏加载速度优势很明显,模板引擎直接输出HTML,不需要浏览器先加载JS再动态渲染。第二,页面权限控制可以在模板里直接用标签判断,比如管理员和普通销售看到的功能入口不一样,直接在ftl文件里用<#if>包裹代码块。第三,开发调试成本低,单个页面从后端数据填充到HTML展示一次完成,不需要关心跨域、API版本、前端路由,写起来非常直白。
我做这套系统时最大的体会是,Freemarker配合Layui后,整个页面没有一行前后端分离项目里那种冗长的fetch或者axios调用,所有接口跳转都是表单提交或者Layui表格自带的URL数据源加载方式。如果团队里没有专业前端,这种模式下后端一个人全干没问题。
2.3 Layui在后台界面中的优势
Layui算是一个比较特立独行的前端框架,没跟Vue、React那条路走,而是靠一套模块化的原生JavaScript组件库打天下。对于CRM这类后台管理系统,Layui的优势集中在三块。
表格组件最为重要,Layui的table模块支持服务端分页,后端接口只需要返回特定格式的JSON:{"code":0, "msg":"", "count":100, "data":[...]},表格组件就会自动分页渲染,配合内置的搜索表单重载功能,做条件筛选非常方便。表单组件提供了一套统一风格的输入框、下拉框、日期选择器,自带校验规则,不用自己写正则。弹层组件layer是另一个高频使用点,新增编辑客户资料、查看跟进记录详情,我都统一用layer.open打开iframe页面,交互清爽且实现简单。
对于偏传统Web开发的程序员来说,Layui的上手成本几乎为零,有jQuery基础就能直接用,遇到需求文档里没写清楚的前端效果,翻翻官方文档十分钟内能搞定。
2.4 ECharts在数据看板中的用法
数据看板是CRM系统的门面,管理层看系统第一眼就是打开首页看图表。ECharts在核心图表的适配度上确实没什么对手。我这个项目里用了四张图:客户行业分布饼图、每月新增客户趋势折线图、商机阶段漏斗图、销售额排行榜横向条形图。
后端需要做的事情,是把SQL统计的结果包装成ECharts需要的JSON结构。比如饼图需要[{name: "制造业", value: 23}, ...],折线图需要横轴一个数组categories、纵轴一个数组data。我写了一个专门的DashboardController,返回一个综合Map,前端Freemarker页面在页面初始化时通过AJAX获取这个Map,然后调用echarts.init初始化图表。这一步要求对ECharts的option配置足够熟悉,尤其是颜色、坐标轴、提示框的细节,直接决定看板的视觉效果。
2.5 Maven与MySQL的基础支撑
Maven在这套系统里担任的是“包工头”角色,所有依赖版本都在pom.xml中集中管住。Spring Boot的依赖不需要手动指定版本,parent里面已经定义好了,但Freemarker、MySQL驱动、Druid连接池、Lombok、PageHelper这些库的版本需要自己核对兼容性。我在搭建项目时踩过一次坑,MySQL驱动用了8.0.30版本,而Spring Boot 2.7默认的数据库驱动版本也对得上,但连接URL必须加上serverTimezone=Asia/Shanghai,否则时间字段会报错。
MySQL设计层面需要特别注意字符集,建库时指定utf8mb4,否则客户名称里存了生僻字或者特殊符号,查询和展示都会变成问号。排序规则用utf8mb4_general_ci足够,数据量不大不需要考虑复杂的collation。
3. 数据库设计思路:从客户到商机的表结构拆解
3.1 用户与角色表设计
用户表是系统的基础,字段包括用户ID、用户名、密码、真实姓名、手机号、邮箱、角色ID、部门ID、状态(启用/禁用)、创建时间、更新时间。密码字段我存的是MD5加盐后的密文,虽然MD5不算强加密,但演示系统足够,生产环境建议换BCrypt。这里有一个细节:不要把密码和用户基本信息放到查询结果里返回给前端,VO类里直接忽略密码字段。
角色表设计的核心思想是权限点控制,而不是简单地把“管理员”写死在代码里。我设计了sys_user、sys_role、sys_user_role三张表,多对多关联权限。页面上的菜单显示通过Freemarker判断用户角色的权限标识,比如${role.permission.contains("customer:export")}来控制导出按钮是否可见。
3.2 客户、联系人、商机、跟进记录的主表关系
客户表(customer)是整个CRM的数据心脏,字段有客户ID、客户名称、统一社会信用代码、所属行业、客户来源、所属区域、客户等级、联系人数量统计、下次跟进时间、创建人ID、所属销售ID、删除标记、创建时间、更新时间。
联系人是挂在客户下的子表,客户ID作为外键。一个客户可以维护多个联系人,这决定了客户详情页里联系人展示的是列表而不是单条数据。商机表保存销售机会,通过客户ID关联客户,同时记录了商机名称、预计成交金额、预计成交日期、当前阶段、赢单概率、备注。跟进记录表是CRM中更新频率最高的表,每次拜访、电话、邮件沟通都要插入一条记录。
这里有一个关键设计:所有业务表都加了deleted字段,逻辑删除而不是物理删除。原因很简单,客户资料和跟进记录一旦物理删除很难恢复,业务上不允许。删除客户时,我会在Service层同步把该客户下的联系人也标记删除,商机和跟进记录保留,查询时统一加where deleted = 0条件。
3.3 数据看板背后的SQL设计
统计报表无论在哪个系统都是数据库性能的重灾区。客户行业分布饼图,只需要一条SQL:select industry, count(*) from customer where deleted = 0 group by industry。每月新增客户趋势,用DATE_FORMAT(create_time, '%Y-%m')分组统计。
商机阶段漏斗图稍微复杂一点,需要对每个状态进行COUNT,同时计算各阶段金额汇总。销售排行榜则是对商机表中的预计成交金额按销售员分组SUM。这些统计如果放在服务端内存里去算,数据量大后会非常卡,能用一条SQL完成的统计就不要在Java里循环聚合。实际开发时,我建议在Dao层用@Select注解直接写SQL,比MyBatis的XML文件更直观。
3.4 索引设计及常见误区
查询性能优化上,主要索引策略是:客户表上对customer_name建普通索引,用于模糊搜索;owner_user_id建索引,因为按销售人员查客户是高频率操作;create_time建索引,支撑时间范围统计。跟进记录表对customer_id建索引,因为详情页要查某个客户的所有跟进记录。
容易踩的坑是:给“行业”这类低区分度的字段建索引,性能提升非常有限;给客户名称加唯一索引时要注意逻辑删除的数据,同一个客户名称被删除后再新增会违反唯一约束。这个问题的解法是把deleted字段纳入唯一索引,或者删除时把名称改成带时间戳的历史名称。
4. 实操全过程:从零搭建到系统跑通
4.1 环境准备与Maven工程初始化
开发环境我建议直接用IDEA,创建项目时选择Spring Initializr,勾选Web、MySQL Driver、Freemarker、MyBatis。如果网络环境访问不了Spring Initializr官方地址,可以把地址换成阿里云镜像,或者先创建一个空Maven项目,再在pom.xml中手动补齐依赖。
最核心的pom.xml依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>4.2 核心配置文件
application.yml是整套系统的中枢配置。我把数据源、模板引擎、静态资源映射、MyBatis配置全部放在这个文件里。
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/crm_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 freemarker: template-loader-path: classpath:/templates suffix: .ftl cache: false charset: UTF-8 request-context-attribute: request mvc: static-path-pattern: /static/** mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.crm.entity configuration: map-underscore-to-camel-case: true pagehelper: helper-dialect: mysql reasonable: true这里有几个需要特别注意的地方。map-underscore-to-camel-case开启后,数据库字段customer_name才能自动映射到Java类的customerName属性,否则查出来的数据所有驼峰命名字段都是null。Freemarker的cache: false在开发阶段一定要开,否则改了模板文件不刷新,浏览器看到的永远是旧页面。request-context-attribute配置后,模板里可以直接用${request.contextPath}获取项目根路径,写静态资源URL时非常方便。
4.3 通用响应结构与统一异常处理
前后端交互的JSON格式必须从一开始就统一,否则后期联调会非常混乱。我定义了一个Result类,包含code、msg、data三个字段。code为0表示成功,非0表示各种业务错误。这个格式同时要满足Layui表格组件的要求,Layui的table模块接受的格式恰好是{"code":0, "msg":"", "count":100, "data":[...]},所以无需单独做适配。
统一异常处理用@RestControllerAdvice配合@ExceptionHandler实现,业务异常返回code=500和错误信息,SQL异常返回code=500加“系统繁忙”提示。需要注意的是,异常处理Controller返回JSON时,如果请求是页面跳转,最好在全局配置类里把freemarker的404和500错误页面指到独立的error.ftl模板,直接展示一堆堆栈信息无论对用户还是对甲方都非常掉价。
4.4 登录会话与权限拦截实现
登录拦截是JavaWeb项目里最常见的需求,我选择的是拦截器加Session的方案,简单可靠,不引入Spring Security的复杂过滤器链。写一个LoginInterceptor实现HandlerInterceptor接口,在preHandle方法里判断Session中是否存在登录用户,不存在就重定向到登录页。注意拦截器默认会拦截所有请求,必须在WebMvcConfigurer中将登录接口、静态资源路径、ECharts数据接口放行。
权限控制上,我在拦截器里会同时把用户角色塞进Session,Freemarker页面直接判断角色枚举。管理员能看到系统管理菜单,主管能看到团队业绩报表,普通销售只能看到自己的客户和商机。Controller层做二次校验,比如删除客户接口必须要求用户角色是管理员,防止越权调用。
4.5 客户管理模块完整实现
客户管理页面是典型的一页式列表页。页面左边是搜索条件框,客户名称文本框、行业下拉框、所属销售下拉框,点击搜索按钮后Layui表格重载并携带参数。页面主体是Layui的table组件,配置了url属性指向/customer/list接口,parseData回调做数据格式处理。
后端Controller接收分页参数page和limit,条件参数customerName、industry等,调用Service层通过PageHelper进行分页查询。PageHelper的使用要注意,必须先执行PageHelper.startPage(page, limit)再执行查询语句,中间不能隔着其他SQL操作,否则分页会失效。我最初写的时候把计数查询放在了startPage和List查询之间,导致分页永远返回全部数据,排查了半天才发现是调用顺序的问题。
Layer弹窗负责新增和编辑,弹窗加载一个独立的add.ftl或edit.ftl页面,提交表单时同步提交隐藏的客户ID字段。删除按钮使用普通form表单POST提交,配合确认弹窗。客户导出功能用了一种讨巧的实现:后端返回application/vnd.ms-excel类型的响应流,把查询结果通过POI写入Workbook然后输出到HttpServletResponse。
4.6 ECharts数据看板接口实现
看板接口我设计成一次请求返回所有图表数据,减少前端请求次数。接口路径/dashboard/statistics,返回Map结构,包含industryPie、customerTrend、opportunityFunnel、rankBar四个key。
后端统计代码的核心是分组查询后转成ECharts需要的格式。饼图的数据结构在Java中用List<Map<String, Object>>组装,每一项包含name和value。折线图的日期数组用List<String>,数量数组用List<Integer>。前端拿到数据后,分别调用四个图表的setOption方法填充数据,并为空数据情况设置占位文案“暂无数据”,避免页面出现空白区域。
看板页面还有一个性能优化点:图表初始化时如果页面处于隐藏状态,浏览器会懒加载导致图表宽度计算错误。解决方法是监听窗口resize事件,调用chart.resize(),页面切换时再触发一次数据加载。这个细节很多人不做,但业务方经常因为最小化窗口回来之后图表变形而投诉。
5. 常见问题与排查技巧实录
5.1 编译期和启动期问题
项目启动报“FreemarkerConfiguration”相关错误,常见原因是Spring Boot 2.7中的Freemarker默认配置与低版本兼容问题。解决方式是把freemarker依赖单独指定版本,或者在application.yml中明确指定spring.freemarker.template-loader-path。
另一个多发问题是启动后访问页面出现404,控制台无异常。多半是模板文件没有放在resources/templates目录下,或者Controller返回的视图名称与文件名不一致。Freemarker对大小写敏感,Customer与customer是两个不同文件,这个错误在Windows上不会暴露,部署到Linux后就会出现,因为Windows文件系统不区分大小写而Linux区分。所以开发阶段就要严格养成文件名小写的习惯。
如果遇到“Consider revisiting the entries above or setting a property”之类Spring Boot自动配置提示,经常是数据源用户名密码错误或者driver-class配置缺失。Druid数据源引入后,还要注意它的防火墙默认配置,如果Druid版本较新且开启了wall过滤器,部分组合SQL会被拦截,比如嵌套子查询比较多时。排查时直接看Druid的log,它把拦截原因打印得非常清楚。
5.2 运行时业务数据问题
查询客户列表时,Layui表格正常渲染但数据显示不全,这是PageHelper分页参数丢失导致。Layui默认的请求参数名是page和limit,后端Controller方法参数也取名page和limit,如果不一致就会拿到null,PageHelper自动count失效。还有一种情况是搜索表单重载时参数没有通过where对象传回后端,所有条件都失效,表现为切换页码数据完全重复。
时间字段显示错误也很常见。MySQL驱动默认返回的Timestamp被Jackson序列化为时间戳数字,前端拿到的是毫秒值,显示出来就是一长串数字。统一处理方式是在application.yml里配置Jackson的时间格式,或者在后端VO里对时间字段标注@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。
Excel导出中文乱码的问题,原因是生成Excel时Workbook创建方式不对,大数据量建议用SXSSFWorkbook保持内存稳定。另外,导出文件名如果直接放在响应头Content-Disposition中,中文需要做URL编码,否则浏览器默认按ISO8859-1解码导致文件名乱码。
5.3 ECharts图表数据不显示
接口返回的数据与图表option结构不匹配是最常见的图表问题。比如饼图要求data是[{name: "制造业", value: 23}],后端返回的却是[{industry: "制造业", num: 23}],此时图表只会渲染空白。解决方法是前端在拿到数据后做一次适配转换,或者后端直接组装成图表需要的格式。我更喜欢后端组装,因为图表结构如果经常变,前端改起来代价更小。
图表高度为0的情况也要注意。ECharts容器div在初始化时如果没有显式高度,图表区域无法渲染。Freemarker页面里<div id="chart" style="width: 100%; height: 400px;">要写死高度,尤其是放在Layui的tab切换页中时,隐藏的面板内部元素宽度为0,需要触发resize事件。
另一个高频问题是漏斗图数据名称与商机阶段枚举不完全一致。数据库里存的是“初步接触”,图表配置里写的是“初识客户”,两者对不上就导致漏斗只显示部分阶段。我建议阶段名称做成常量类,数据库写入、页面展示、图表配置统一使用同一份常量,避免维护散落的名词。
5.4 部署过程中的实操经验
打包部署时,mvn clean package生成的可执行Jar包可以直接用java -jar启动,但Jar包里的freemarker.yml配置与配置中心交互时经常出现路径问题。Spring Boot读取classpath内的文件没有问题,但如果application.yml里配置了spring.freemarker.template-loader-path: file:/opt/crm/templates,那就必须保证服务器上的该目录真实存在且有权限,否则启动后访问页面报模板找不到。
服务器端口占用也是部署时的经典问题。Spring Boot内嵌Tomcat默认8080端口,如果服务器上已经有Nginx或者旧应用占用,启动会直接失败。建议启动命令加上--server.port=8888或者修改配置文件。生产环境我还会加一条JVM参数-Dfile.encoding=UTF-8,避免中文乱码问题在服务器上出现。
值得一提的是,整套系统打成Jar包后大约50MB左右,部署比传统的war包省事很多,不用安装外部Tomcat,只要服务器有JDK 8以上就能跑。如果项目要挂到公网,记得把Druid控制台和Spring Boot Actuator的暴露端点关掉,否则管理接口直接暴露在公网上非常危险。
6. 个人实操心得与后续扩展建议
6.1 项目落地后的维护体会
这套系统从开发到部署,给我最大的感触是“稳定压倒一切”。Spring Boot加Freemarker加Layui的组合理念虽然不够新潮,但是每个环节都有非常成熟的解决方案,遇到了问题搜索引擎上一搜就是现成答案。相比之前用前后端分离做的项目,省掉了Node环境、跨域配置、接口文档维护、前端打包这些步骤,单人开发效率提升非常明显。
关于数据治理,逻辑删除字段配合唯一索引在CRM系统里如果有数据重复的需求,务必要设计好删除策略。实际操作中我发现客户重复录入才是CRM最常见的脏数据来源,建议在Service层增加查重逻辑,客户名称在所属销售范围内不能重复,辅以模糊查询提示“相似客户已存在”,体验会好很多。这个查重逻辑用一条SQL加一个后端判断就能实现,代码量不大,但业务价值很高。
6.2 可扩展方向
系统跑通之后,如果要往生产级方向扩展,优先级比较高的方向有三个。第一是引入消息或定时任务,比如每天定时给销售推送“今日到期应收”和“3天未跟进客户”提醒,避免销售遗漏重要客户。第二是增加邮件通知模块,当客户被分配给新销售时,自动发送一份交接邮件,里面带上客户最近三条跟进记录。第三是把权限体系从简单的角色升级成细粒度的数据权限,比如主管只能看本部门数据,销售只能看自己负责的客户,这个通过拦截器里追加SQL条件过滤就能实现,不需要改动前端。
做完这套CRM之后,我对业务系统的理解也深了一层。技术永远是手段,真正有价值的是业务逻辑背后的规则。客户分级怎么定,跟进频率怎么提醒,商机阶段怎么流转,这些才是客户关系管理的灵魂,框架和代码只是把这些规则落地。如果你正准备做同类项目,我建议不要在技术栈上纠结太久,先想清楚业务规则,再动手写代码,效率会高很多。