
简介基于SSM的客户资料管理系统客户关系管理系统是一套面向计算机相关专业毕业生与Java学习者的完整毕设项目采用Spring、SpringMVC、MyBatis搭建后台MySQL存储数据JSP设计页面经严格调试确保可直接运行。资源包含3个文件项目源码压缩包、数据库脚本sql及项目说明文档txt整体约25.18MB解压即可导入Eclipse配合Tomcat使用。系统围绕管理员、客户经理、营销主管三种角色划分权限覆盖员工信息、产品信息、客户信息、服务信息、交易信息等管理模块并支持客户来源、支付方式、产品类型、职位信息、服务类型、客户等级及开发进度等基础数据维护功能齐全、界面美观、操作便捷。目前已有1112人学习下载适合正在准备毕业设计或希望进行SSM项目实战练习的读者通过源码与脚本快速理解业务逻辑与分层架构。1. 客户资料管理这个毕设为什么八成选 SSM毕设答辩现场老师常问的一句话是“你这段客户查询的代码从页面点下去到数据库返回中间走了几个环节”很多同学就是在这里卡壳的。基于 SSM 的客户资料管理系统说白了就是一套 Spring SpringMVC MyBatis 的三层项目源码外加一份能直接建库建表的数据库脚本。它解决的是客户信息“录得进去、查得出来、改得动、删得干净”的问题附带登录、分页、权限这类能让答辩老师点头的功能点。这套东西适合两类人一类是正在做毕设、想找一份能跑通的 Java Web 项目当底子的学生另一类是手头需要给内部团队搭一个轻量客户管理后台、但不想一开始就上 Spring Boot 全家桶的入门开发者。下面按数据库脚本、源码导入、SSM 配置链路、常见启动故障这条线往下走你照着做完能把项目从压缩包变成自己机器上一个能点的系统。2. 为什么是 SSM三个框架的分工和一次客户请求的完整流转2.1 Spring 管对象、SpringMVC 管分发、MyBatis 管 SQL做这个项目之前得先把三个框架各自盯哪块搞清楚。Spring 负责对象生命周期和事务你在 service 实现类上标一个 Service在增删改方法上标 TransactionalSpring 容器启动时扫描注解把这些 bean 管理起来方法执行过程中遇到异常就回滚数据不会插一半留一半。数据源、SqlSessionFactory 这类基础设施也挂在 Spring 的 IoC 容器里后面想换数据库环境只改 jdbc.properties 就行不用动 Java 代码。SpringMVC 是表现层的分发器。它的 DispatcherServlet 像前台接待任何以 /customer 开头的请求都由它决定交给哪个 Controller 方法处理方法返回逻辑视图名如 customer_list视图解析器再拼出 /WEB-INF/jsp/customer_list.jsp 的物理路径。MyBatis 则管数据库访问层它把 Mapper 接口里的方法名和 XML 里的 SQL 绑定比如 selectAll 对应一条 SELECT * FROM crm_customer执行完自动把结果集映射成 List 省掉 JDBC 里手动 getString、setString 的一堆琐碎代码。这套组合到今天依然是毕设选题里的常青树一个重要原因是它的配置全部摆在明面上。Spring Boot 帮你自动装配掉的那些东西在 SSM 里都要自己声明一次虽然麻烦但特别适合用来回答“这里为什么这么配”这类答辩问题。2.2 点击“添加客户”后请求走过的五站拿最常见的“添加客户”场景拆一次请求流转。你在客户列表页填好姓名、电话、公司点了保存按钮浏览器发出 POST /customer/add请求头 Content-Type 是 application/x-www-form-urlencoded表单体是 name张三phone138xxxxcompany某公司。第一站是 web.xml 里注册的 DispatcherServlet。它的 url-pattern 通常是 /注意不是 //会把 JSP 和静态资源也拦进 SpringMVC导致页面直接白屏。第二站是 HandlerMappingSpringMVC 根据 /add 找到 CustomerController 里对应的处理方法从 IoC 容器取出 CustomerService 实例。第三站是参数绑定SpringMVC 利用反射把表单字段按 name 属性填充到 Customer 对象的同名属性上如果你表单里有日期字段得在实体属性上加 DateTimeFormat(patternyyyy-MM-dd)不然类型转换直接报 400。第四站是事务。CustomerService 的 add 方法标了 Transactional方法内部调用 customerMapper.insert(customer)MyBatis 参数映射把 Customer 对象的属性挨个填进 insert 语句的 #{} 占位符PreparedStatement 执行返回受影响行数。第五站是控制器收到结果后 redirect:/customer/list浏览器跳回列表页新插的那行数据出现在表格里。这五个环节任何一个断了表现都是白屏或 404但排查路径完全不同后面避坑章节会逐个展开。2.3 源码包结构controller、service、mapper 各层怎么对应拿到一份客户资料管理系统的源码先别急着跑花五分钟把包结构过一遍crm-customer ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/example/crm │ │ │ ├── controller # 接收请求、参数校验、返回视图 │ │ │ ├── service # 业务接口 │ │ │ │ └── impl # 接口实现写业务逻辑和事务 │ │ │ ├── mapper # MyBatis Mapper 接口 │ │ │ ├── entity # 和表字段对齐的实体类 │ │ │ └── common # 分页封装、统一返回结果 │ │ └── resources │ │ ├── spring # Spring、SpringMVC、MyBatis 配置文件 │ │ ├── mapper # Mapper 接口对应的 XML 文件 │ │ ├── jdbc.properties │ │ └── log4j.properties │ └── webapp/WEB-INF │ ├── web.xml │ └── jspentity 里的 Customer.java 每个字段对应 crm_customer 表的一个列驼峰命名法MyBatis 里开启 mapUnderscoreToCamelCase 后数据库的 create_time 能自动映射到 createTime大多数据源不用手写 resultMap。mapper 目录下是接口和 XML 两个文件接口定义方法XML 写 SQL两者靠 namespace 和语句 id 对应例如 namespace 写成 com.example.crm.mapper.CustomerMapperXML 里的 select 标签 id 写成 selectAll和接口方法名一致。service 接口和实现类分开写是这个项目的分层惯例Controller 不直接碰 Mapper所有列表查询都先走 service 层分页参数和业务判断放在 impl 里。common 包里通常有 PageResult 和 Result 两个类前者封装当前页数据、总数和页码后者统一返回 code、message 和 dataJSP 里用 JSTL 直接从这些对象上取字段。3. 数据库脚本先落地客户表怎么建、脚本怎么导、连接怎么改3.1 一库三表的结构客户主表、跟进记录表、用户表客户资料系统的数据库脚本核心是三张表。客户主表 crm_customer 存客户本身的信息跟进记录表 crm_follow_up 存业务员每次沟通的内容因为一个客户会有多次跟进单独建表才能把时间线留住而不是在客户表里挤一个字段覆盖掉历史用户表 sys_user 撑起登录模块答辩时不用现场在数据库里手敲账号。-- 客户主表 CREATE TABLE crm_customer ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 客户姓名, phone varchar(20) DEFAULT NULL COMMENT 联系电话, email varchar(100) DEFAULT NULL COMMENT 邮箱, company varchar(100) DEFAULT NULL COMMENT 公司名称, source varchar(20) DEFAULT 手动录入 COMMENT 客户来源渠道, category tinyint(4) DEFAULT 1 COMMENT 1潜在 2意向 3成交 4流失, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_company (company), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户资料表;这里有三处值得留意的设计。第一category 用 tinyint 而不是 varchar把客户状态固定成“潜在、意向、成交、流失”四个枚举值程序里用常量或者枚举类去对照比直接存汉字字符串省空间也避免同一个意思被录成“意向客户”和“有意向”两种写法。第二phone 和 company 各加了一个普通索引因为列表页最常见的两个筛选条件就是按电话搜和按公司搜索引让 SELECT 查询不至于全表扫。第三create_time 和 update_time 都给了默认值插入数据时 Java 代码里不 set数据库也会自动填省一层没必要的逻辑。-- 跟进记录表 CREATE TABLE crm_follow_up ( id int(11) NOT NULL AUTO_INCREMENT, customer_id int(11) NOT NULL COMMENT 客户ID, content varchar(500) NOT NULL COMMENT 跟进内容, next_time datetime DEFAULT NULL COMMENT 下次跟进时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_customer (customer_id), CONSTRAINT fk_follow_customer FOREIGN KEY (customer_id) REFERENCES crm_customer (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT跟进记录表;跟进表加了外键约束和 ON DELETE CASCADE意思是客户主表删除一条客户数据时这个客户名下所有的跟进记录一起删掉不用在业务代码里写两条 delete。毕设里这个设计很加分但在 MySQL 8.0 上要注意外键名 fk_follow_customer 在同一个库内必须唯一如果你后面重建表换个前缀再执行。-- 系统用户表 CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(64) NOT NULL COMMENT MD5 密文, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role varchar(20) DEFAULT admin COMMENT 角色, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; INSERT INTO sys_user (username, password, real_name) VALUES (admin, MD5(123456), 管理员);密码字段存的是 MD5 后的密文不是明文登录时把用户输入的密码也做一次 MD5 再比对。这是最基础的密码处理比明文强很多。脚本末尾预置了一条 admin 账号密码是 123456导入后直接拿这台账号去登录后台不用自己现写 SQL 塞数据。3.2 导入脚本命令行写法和 IDEA 里 Database 面板的用法拿到数据库脚本文件后常见的导入方式有两种。命令行适合你只想快速建库建表mysql -u root -p -D crm --default-character-setutf8mb4 crm.sql这里的 -D crm 指定目标数据库前提是库已经存在如果脚本里自带 CREATE DATABASE IF NOT EXISTS crm那就不用写 -D直接 mysql -u root -p crm.sql 也行。--default-character-setutf8mb4 是防止 Windows 命令行默认字符集是 GBK导致脚本里的中文注释乱掉。导入完用 mysql -u root -p -e SHOW TABLES FROM crm; 检查一下三张表都在不在。如果你习惯在 IDEA 里操作顺序是右侧栏 Database → → Data Source → MySQL填 host、port、user、passwordDatabase 一栏写 crm 或留空连接成功后右键数据库节点 → Run SQL Script…选你的 crm.sql右下角能看到执行日志。IDEA 默认会用 UTF-8 读脚本文件比命令行省心。脚本的导出和导入是一个来回右键 schema → Dump to File → SQL DDL or Data就能把建表语句和数据一起导出成新脚本这也是源码包里数据库脚本的常见来源。3.3 jdbc.properties 三行参数决定本地能不能连上库SSM 项目里数据库连接信息通常抽在 jdbc.properties 这个文件里Spring 的配置文件再去引用它。拿到源码先改这里这一步不做完后面启动永远是红的jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/crm?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的数据库密码driver 一项MySQL 8 必须写 com.mysql.cj.jdbc.Driver老版本驱动 com.mysql.jdbc.Driver 连 MySQL 8 会直接报驱动类和认证方式不匹配。url 里那几个参数不是装饰useUnicodetruecharacterEncodingutf8 控制中文不乱码useSSLfalse 关掉 SSL 握手serverTimezoneAsia/Shanghai 解决 MySQL 8 的时区报错allowPublicKeyRetrievaltrue 解决 caching_sha2_password 认证方式下的公钥获取问题。username 和 password 改成你本地 MySQL 的实际账号注意密码不要写进 git 提交记录这是这条配置文件的底线。4. 源码导入 IDEA从 pom.xml 到 Tomcat 启动的全过程4.1 版本对照表环境搭错后面全是玄学SSM 项目跑不起来十有八九是环境版本不对付。先把版本对应关系摆出来组件推荐版本说明JDK1.8SSM 毕设最常见配置lambda、Stream 写起来顺手Maven3.6.x3.9 也能用3.5 以下可能拉不全依赖Tomcat8.5对应 Servlet 3.1和 javax.servlet 包名匹配MySQL5.7 / 8.08.0 注意驱动和 url 参数见 3.3 节IDEA2020 以上社区版足够不用刻意找旗舰版里面最容易翻车的是 Tomcat 10。Tomcat 10 已经把 javax.servlet 包名换成了 jakarta.servlet而这个项目源码里所有 import javax.servlet 的地方都会编译不到启动直接 NoClassDefFoundError。Tomcat 8.5 和 9 都还在 javax 时代拿来跑 SSM 最稳。4.2 IDEA 导入 Maven 项目先把这三处配置做对打开 IDEAFile → New → Project from Existing Sources定位到项目根目录选中 pom.xml 作为导入入口之后一直 Next。导入完成后右下角 Maven 面板会自动开始下载依赖第一次可能持续几分钟等它安静下来再看代码不要急着点运行。然后做三件小事第一Project Structure → Project 里 SDK 选 1.8Language level 也选 8避免源码里用了高版本语法编译不过。第二右键 src/main/resources 目录 → Mark Directory as → Resources Root这一步容易漏不标记的话 mybatis 的 mapper XML 不会进入编译产物运行时报 Invalid bound statement。第三确认 pom.xml 里的依赖版本互相兼容核心依赖长这样dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependencyspring-webmvc 和 spring-jdbc 这些同族依赖版本最好保持一致不要一个 5.3 一个 4.3 混着用Spring 容器加载时会出现 AbstractMethodError 这类很难从字面看出原因的报错。mybatis-spring 2.0.x 匹配 Spring 5如果你源码里用的是 mybatis-spring 1.xSpring 5 下会有兼容问题。mysql-connector 的版本要跟着你本地 MySQL 的主版本走5.7 用 5.1.49 没问题8.0 建议 8.0.28 或更新。4.3 Tomcat 配置和启动验证看日志比看页面靠谱IDEA 里部署 SSM 项目通常走 Run → Edit Configurations → → Tomcat Server → LocalTomcat 目录选你本地的 8.5Deployment 页签点 把当前项目以 war exploded 方式加进去Application context 填 /crm。war exploded 比 war 包多一个好处改了 JSP 或静态资源保存后自动生效不用重启整个 Tomcat。配置完成后启动不要急着看浏览器先把启动日志翻一遍tail -f /你的tomcat目录/logs/catalina.outIDEA 的控制台输出有时会吞掉一部分异常catalina.out 里通常记得更全。看到 “Deployment of web application archive ... has finished in xx ms” 且没有 SEVERE 字样再访问 http://localhost:8080/crm/login.jsp。如果页面 404先确认 Application context 是不是 /crm再确认 jsp 文件确实在 src/main/webapp/WEB-INF/jsp 下路径对不上就会绕进 Tomcat 的默认 404 页。提示如果访问登录页正常但点登录提交后跳转 404问题大概率在 SpringMVC 控制器映射或视图解析器前缀配置上等到第 5 章的排查步骤再处理。第一步先把静态 JSP 页面跑通。5. 避坑记录SSM 客户资料系统 5 个最容易翻车的现场这一节写的是带这个方向最常见的五个问题每个都是“现象 → 原因 → 解决”的格式你在自己机器上复现这个项目时碰到任何一个照着定位就行。5.1 启动直接崩溃ClassNotFoundException 第一嫌疑是依赖没进 WEB-INF/lib现象Tomcat 启动后秒停catalina.out 里报 java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener。原因IDEA 里 Run Tomcat 时项目 Artifact 没有把 Maven 依赖同步到 WEB-INF/lib。spring-webmvc 依赖信息虽然在 pom.xml 里但部署产物里根本没有这个 jarTomcat 自然加载不到监听器。解决Project Structure → Artifacts → 选中当前项目对应的 Artifact → Output Layout 页签右边 Available Elements 列表里展开 Maven 依赖全选后拖到左边 WEB-INF/lib 下重新 Build Artifact 再启动。如果你看到 Artifact 类型是 war exploded 但 WEB-INF 下没有 lib 目录手动建一个 lib 目录再把所有依赖 jar 拷进去也行。确认部署描述符里 WEB-INF/lib 存在且里面有 jar 文件再点启动按钮。5.2 页面显示中文变问号三个层级字符集全查一遍现象页面录入“张三”表单提交后回到列表页显示“”或者数据库里直接存了问号。原因这是三层字符集同时出了岔子。第一层连接数据库的 JDBC URL 没带 characterEncodingutf8MySQL 连接按系统默认字符集走Windows 下常见是 GBK第二层建表时没用 utf8mb4表默认继承库的 latin1 或 utf8第三层写了 JSP 页面本身的编码不是 UTF-8。解决先修 URL在 jdbc.url 后面加 useUnicodetruecharacterEncodingutf8再把脚本里所有表统一改成 utf8mb4已经建好的表可以执行ALTER TABLE crm_customer CONVERT TO CHARACTER SET utf8mb4;最后把 JSP 第一行声明 % page contentTypetext/html;charsetUTF-8 languagejava % 补上。三层都对齐后重开一个连接写入问题就消了。注意已经乱掉的存量数据不会自动修复要重新录入或跑一次 UPDATE 修正。5.3 Mapper 接口报 Invalid bound statementnamespace 和 XML 位置双双背锅现象Tomcat 能正常启动点“客户列表”时抛 BindingException: Invalid bound statement (not found): com.example.crm.mapper.CustomerMapper.selectAll。原因三个候选。第一Mapper XML 的 namespace 没写成接口的全限定名写成了 CustomerMapper 这种短名第二mapper XML 文件没有放在 mybatis 配置扫描到的路径下比如 resources/mapper 和配置里的 mapperLocations 不匹配第三同名 XML 没有被 IDE 标记成资源目录编译产物里直接没有这个文件。解决打开 mapper XML 文件核对 namespace 属性mapper namespacecom.example.crm.mapper.CustomerMapper再看 Spring 配置文件里 SqlSessionFactoryBean 的 mapperLocationsproperty namemapperLocations valueclasspath*:mapper/*.xml/classpath* 和 classpath 的区别在于多模块工程毕设单模块用 classpath:mapper/.xml 足够但 classpath: 更保险。最后去项目的 target/classes/mapper 目录看一眼文件在不在不在就是资源标记问题回到 4.2 小节把 resources 目录标记成 Resources Root。5.4 JSP 页面把 ${list} 原样打印出来web.xml 版本声明惹的祸现象打开列表页页面上直接出现 ${list} 这种字符串客户表格是空的但后台接口明明返回了数据。原因web.xml 头部声明的 Servlet 版本是 2.3 或 2.4这两个版本默认 isELIgnoredtrueJSP 里的 EL 表达式不做解析当成普通文本原样输出。Tomcat 8.5 支持 Servlet 3.1但 web.xml 没升级JSP 容器就按旧规则走。解决把 web.xml 的根节点声明整段替换成下面的 Servlet 3.1 规范头web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1顺带检查页面里用了 JSTL 的 c:forEach 却没有引入 JSTL 依赖的情况那种会直接 500报错信息比 EL 不解析明显得多。加了 jstl 1.2 还不识别标签确认 JSP 顶部有 % taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。5.5 MySQL 8 连不上Public Key Retrieval 和 SSL 的连环坑现象项目启动时数据库连接失败报 Public Key Retrieval is not allowed或者 SSL connection error: No appropriate protocol。原因MySQL 8 默认身份认证插件是 caching_sha2_password非 SSL 连接下第一次连接需要从服务端获取公钥JDBC URL 里没开放这一项就拒绝连接。同时老版本驱动在 MySQL 8 的 SSL 握手阶段容易报协议不支持。解决把 JDBC URL 改完整一点jdbc.urljdbc:mysql://localhost:3306/crm?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaicharacterEncodingutf8驱动同步换成 com.mysql.cj.jdbc.Driver。如果改了还是连不上把 MySQL 用户认证方式降级回 mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;改完之后重启项目。这一组问题在 5.7 上基本不会出现但 8.0 上是必踩拿到源码第一件事就把它处理掉。6. 给系统加一个分页模糊查询让答辩多一个可聊的亮点客户资料系统的核心操作就是列表查询。源码里多半已经有一条简单的 selectAll但只做全量查询是不够的客户数据超过几百条时一次加载所有数据会让页面卡顿答辩老师大概率会追问一句“上万条客户你怎么找”。加一个带条件的分页查询是最值得的改造。6.1 分页查询的 XML 与 Service 写法在 CustomerMapper.xml 里增加一个动态 SQL 查询按关键词模糊匹配姓名、公司和电话同时做物理分页select idselectByCondition resultTypecom.example.crm.entity.Customer SELECT * FROM crm_customer where if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR company LIKE CONCAT(%, #{keyword}, %) OR phone LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select这里的 标签会自动处理第一个条件前面的 ANDkeyword 为空时输出的 SQL 就是不带 WHERE 的全量分页查询不会出现语法错误。LIKE 用 CONCAT 拼接百分号而不是直接在参数里写 %param%是为了保证 #{} 预编译生效参数只作为值传入没有 SQL 注入风险。offset 由 Service 层算好offset (pageNum - 1) * pageSize页面上传 pageNum 和 pageSize 两个参数Controller 收下后传给 service。配套的还要有一个 count 查询分页器才能算出总页数。count 语句的 WHERE 条件必须和列表查询完全一致否则“总数是全部客户、列表是过滤后的客户”分页条就会算出奇怪的页码。6.2 用 JMeter 验证分页接口是否抗压改造完之后拉一个 JMeter 建个分页压测脚本线程组设 50 个并发HTTP 请求指向 /customer/list?pageNum1pageSize10加一个响应断言判断 HTTP 200跑完后看聚合报告的响应时间中位数和异常率。这个步骤能验证分页 SQL 有没有隐性问题比如缺索引导致页码越深越慢也能让答辩时你有话说比如“我压过 50 并发平均响应时间稳定在几百毫秒”。我第一次带毕设时学生交的版本只有列表没有查询条件老师问一句“一千个客户你怎么找到那个电话”就哑在台上。后来加上这条动态 SQL整个演示才真正聊起来——从 MyBatis 的 #{} 预编译讲到索引再讲到分页参数边界。我的习惯是源码拿到手先不急着跑花半小时把表结构和查询条件读一遍补一个筛选进去这个过程比背十遍框架原理都值。分页边界记得检查页码小于 1 的情况传 0 或负数时让 Service 直接回退到第一页这个细节很多人会漏希望帮到你。本文还有配套的精品资源点击获取