news 2026/10/27 15:19:23

基于SSM的客户资料管理系统设计与实现:Spring+SpringMVC+MyBatis完整源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SSM的客户资料管理系统设计与实现:Spring+SpringMVC+MyBatis完整源码解析

简介:SSM(Spring+SpringMVC+MyBatis)是Java Web领域最经典的轻量级分层开发组合之一,它将请求接收、业务处理与数据持久化拆分为清晰的三层架构。理解这一原理,有助于快速掌握企业级项目的基本组织方式。基于此架构的客户资料管理系统,往往涵盖用户登录、客户档案管理、跟进记录与权限控制等核心需求,配合数据库脚本与项目源码,可显著降低毕业设计与工程练手的起步门槛。在实际部署中,涉及Maven环境配置、MySQL脚本导入、Tomcat运行、中文编码及静态资源拦截等常见问题,围绕这些环节的排查与优化,能帮助开发者从“跑通CRUD”迈向“理解分层设计与安全边界”。这是一套基于SSM的客户资料管理系统完整方案,从表结构设计到代码实现、再到答辩加分项,适合计算机专业毕业设计选型及初级开发者参考。

1. 客户资料管理系统为什么还值得用 SSM 做:毕设选型与三个现实理由

每年毕业季都有大量同学卡在"系统选型"这一步,客户资料管理系统这种题目看似简单,真下手才发现入口很多:有 Spring Boot + MyBatis Plus 的,有 SSM 传统三层结构的,甚至还有前端用 Vue 后端用 Node 的。那标题里写的"基于 SSM 的客户资料管理系统【项目源码+数据库脚本】"到底好在哪?一句话说清楚:它是一个把 Java 后端最经典的 Spring + SpringMVC + MyBatis 三件套串起来的完整 CRUD 业务系统,自带源码和数据库脚本,目标用户是准备做毕设的计算机相关专业学生,以及想补一套"标准三层架构"作为练手项目的在职初级开发。

选它的理由也很现实。其一是答辩友好,SSM 是教材和面试里出现频率最高的 Java Web 组合,评审老师看到这个技术栈会觉得你基本功扎实,不用解释太多。其二是复杂度适中,客户资料管理涉及用户登录、客户信息增删改查、分页搜索、数据统计,规模刚好处在"能展示完整思路又不至于写不完"的区间。其三是源码加数据库脚本的交付形式,意味着你不用从零建表,省掉最枯燥的起步阶段,可以把精力放在跑通流程和准备答辩问题上。这篇文章就按"系统拆解→环境搭建→代码实现→数据库脚本→踩坑排查→加分优化"的顺序,把这个标题背后的方案完整讲清楚,你要是手里已经有一套源码,按章节对照补课就行;要是还没开始做,也能照着我给的路径自己复现一套。

2. 拆解 SSM 客户资料管理系统的功能清单与表设计:登录、客户档案、跟进记录这三块怎么落地

2.1 先搞清楚"客户资料管理"到底管哪些数据:人员角色与数据流向

很多同学拿到标题就开始搜代码,其实第一步应该想清楚"这个系统给谁用、每天在操作什么"。一套标准的客户资料管理系统,用户角色至少分两类:管理员和普通业务员。管理员负责维护系统里的用户账号、查看全部客户数据;业务员负责录入自己名下的客户、更新跟进状态、记录每次沟通内容。少数版本还会加一个"部门经理"角色,能看到下属业务员的客户列表,这可做可不做,看你的源码包里有没有。

数据流向概括起来就是:业务员登录后,在客户档案模块新增一条客户记录,填写公司名称、联系人、电话、地址、所属行业、客户等级;后续每次电话或拜访后,在跟进记录模块追加一条沟通纪要;客户成交了,就把客户状态从"跟进中"改成"已成交"。管理员不定时登录后台,看客户总量、按行业或来源统计分布,必要时把某条数据导成 Excel 存档。这个链路不复杂,但它把 SSM 三层架构里每一层该干什么都覆盖到了:Controller 接收请求、Service 处理业务规则、Mapper 操作数据库。

2.2 核心表结构:用户表、客户表、跟进记录表的设计思路与字段说明

数据库脚本通常包含至少三张核心表:sys_user(用户表)、customer(客户表)、follow_record(跟进记录表),有的项目还会加 customer_type(客户类型字典表)和 login_log(登录日志表),用来丰富功能。下面是我按常见毕设源码的建表习惯整理的一个字段集,你可以拿它跟你的数据库脚本做对照。

-- 用户表 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT 'MD5加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', role_id INT DEFAULT 2 COMMENT '角色:1管理员 2业务员', status TINYINT DEFAULT 1 COMMENT '账号状态:1正常 0停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 客户表 CREATE TABLE customer ( id INT PRIMARY KEY AUTO_INCREMENT, customer_name VARCHAR(100) NOT NULL COMMENT '公司/客户名称', contact_person VARCHAR(50) COMMENT '联系人', contact_phone VARCHAR(20) COMMENT '联系电话', email VARCHAR(100) COMMENT '邮箱', address VARCHAR(200) COMMENT '地址', industry VARCHAR(50) COMMENT '所属行业', customer_level TINYINT COMMENT '客户等级:1高 2中 3低', status TINYINT DEFAULT 1 COMMENT '客户状态:1跟进中 2已成交 3已流失', owner_id INT COMMENT '归属业务员ID,关联sys_user.id', remark VARCHAR(500) COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 跟进记录表 CREATE TABLE follow_record ( id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL COMMENT '客户ID', user_id INT NOT NULL COMMENT '跟进人ID', content VARCHAR(1000) COMMENT '跟进内容', next_plan VARCHAR(500) COMMENT '下次跟进计划', follow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '跟进时间' );

这段建表脚本的精髓在 customer 表的设计上:owner_id 用外键关联到 sys_user 表,这样每一条客户数据都能找到归属的业务员,管理员按人筛选客户时只需要一条 where 条件;status 字段用数字表示状态,而不是存中文,是为了后续做统计分析时方便 group by。注意客户等级和行业如果做成字典表,后续加选项就不用改表结构,直接在字典表里插一行,但代价是查询时要多一次 join,毕设项目数据量小,直接用字符串字段也说得通。

2.3 页面功能与三层架构的对应关系:登录拦截、客户列表、新增编辑、删除的请求流转

搞清楚了表和角色,下一步是看请求在 SSM 框架里怎么流转。以"新增客户"这个动作为例,完整链路是这样的:浏览器提交表单到 CustomerController 的 addCustomer 方法,Controller 接收参数并封装成 Customer 对象;Service 层先校验客户名称是否为空、电话格式是否合法,再调用 CustomerMapper 的 insert 方法写库;MyBatis 执行 SQL 后把受影响行数返回,Service 判断结果,Controller 再跳转回客户列表页并携带提示信息。

这里有个常见理解误区:很多同学以为 SSM 就是三个框架拼在一起,写代码时把 SQL 直接写在 Controller 里,这是不对的。SSM 的分层价值在于——业务规则写在 Service 里可以复用,SQL 写在 Mapper 的 XML 文件里方便单独调优,Controller 只负责参数接收和页面跳转。你的毕设源码如果不是这么分的,建议你重构一下,因为答辩时老师很爱问"为什么要分三层",你要是答不上来会比较尴尬。

2.4 用一张表看清你的源码包结构:mapper 层、service 层、controller 层各自该放什么

判断一套 SSM 客户管理系统源码质量好坏,最快的方法是打开源码包看目录结构。合格的工程一般长这样:

src/main/java/com/example/crm ├── controller # SpringMVC 控制器,接收请求、返回视图 ├── service # 业务接口 + 实现类,处理业务逻辑 ├── dao/mapper # MyBatis 的 Mapper 接口 ├── entity/pojo # 实体类,对应数据库表 ├── interceptor # 登录拦截器,做权限校验 └── util # 工具类,如 MD5 加密、分页封装 src/main/resources ├── mybatis # MyBatis 的 XML 映射文件 ├── spring # Spring 和 SpringMVC 的配置文件 └── jdbc.properties # 数据库连接配置 src/main/webapp ├── static # 静态资源 js/css/images └── WEB-INF/jsp # JSP 视图页面

如果你的源码包跟这个结构有出入,问题一般不大,重点是检查三件事:第一,mybatis 的 XML 文件里 SQL 是否都写了参数占位符而不是字符串拼接,防止 SQL 注入是答辩安全问题的标准考点;第二,Service 层是否有事务注解,没有的话多表操作容易造成数据不一致;第三,web.xml 里 DispatcherServlet 的映射路径是 / 还是 *.do,这直接决定静态资源能不能被正常访问。这三处没问题,工程地基就稳了。

3. 用 Spring + SpringMVC + MyBatis 把客户 CRUD 跑通:最小可运行配置与三步验证

3.1 环境版本怎么选:JDK 8 + Maven + Tomcat 8/9 的最稳组合

拿到源码第一步是先配环境,这一步翻车率奇高。常见的错误是 JDK 版本装得太新,比如用 JDK 17 跑老项目,编译时直接报错,因为老版本 Spring 对高版本 JDK 的字节码格式支持不好。SSM 项目通常诞生在 JDK 8 时代,老老实实装 JDK 8 是最稳的。Maven 用 3.6 左右的版本就好,Tomcat 选 8.5 或 9.0,这三个版本组合是被验证过无数次的"不会吵架"的组合。

开发工具方面,IDEA 需要安装 Lombok 插件——大多数毕设源码的实体类都会用 Lombok 的 @Data 注解来免写 getter/setter,不装插件会报编译错误。另外注意导入工程时要用"从现有源码导入 Maven 项目"的方式,而不是直接 Open 文件夹,否则 Maven 依赖不会自动下载。导入后先让 Maven 把依赖拉完,确认 pom.xml 里没有红色报错,再进行下一步。

3.2 数据库连接配置:jdbc.properties 里改这五个参数,附驱动版本对照

数据库脚本到手之后,第一步是修改 jdbc.properties 里的连接参数。这个文件一般在 src/main/resources 目录下,内容大致如下:

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/crm_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456 jdbc.maxActive=50

这里有两个坑必须注意。第一个坑是 MySQL 驱动版本和数据库版本的匹配问题:如果本机装的是 MySQL 5.7 及以下,用 com.mysql.jdbc.Driver 没问题;如果是 MySQL 8.0+,驱动类要改成 com.mysql.cj.jdbc.Driver,而且 pom.xml 里 mysql-connector-java 的版本要升级到 8.x,否则会报"Loading class is disallowed"之类的连接异常。第二个坑是 serverTimezone 参数,MySQL 8.0 默认时区跟本地不一致,连接时容易报错,加上 Asia/Shanghai 基本能消掉这个报错。

3.3 跑通客户列表的最小链路:Controller、Service、Mapper 三层代码逐行注释

配置改好后,找一个最简单的功能把整条链路验证通。我建议先跑"客户列表查询",因为它不需要表单提交,也不涉及复杂参数。下面是一整套最小代码,你对照着看业务是怎么在层与层之间走的。

// 实体类,对应 customer 表 @Data public class Customer { private Integer id; private String customerName; private String contactPerson; private String contactPhone; private String industry; private Integer status; private Integer ownerId; }
// Mapper 接口 public interface CustomerMapper { // 查询所有客户,参数是当前登录用户ID,管理员传0表示查全部 List<Customer> selectCustomerList(@Param("ownerId") Integer ownerId); }
<!-- MyBatis 映射文件 CustomerMapper.xml --> <select id="selectCustomerList" resultType="com.example.crm.entity.Customer"> SELECT * FROM customer <where> <if test="ownerId != null and ownerId != 0"> AND owner_id = #{ownerId} </if> </where> ORDER BY create_time DESC </select>
// Service 接口 + 实现类 public interface CustomerService { List<Customer> getCustomerList(Integer ownerId); } @Service public class CustomerServiceImpl implements CustomerService { @Autowired private CustomerMapper customerMapper; @Override public List<Customer> getCustomerList(Integer ownerId) { // 业务规则:管理员传0查全部,业务员只能看自己的 return customerMapper.selectCustomerList(ownerId); } }
// Controller @Controller @RequestMapping("/customer") public class CustomerController { @Autowired private CustomerService customerService; @RequestMapping("/list") public String list(@RequestParam(value = "ownerId", defaultValue = "0") Integer ownerId, Model model) { // 调用 service 层拿数据,放进 Model,交给 JSP 渲染 model.addAttribute("customerList", customerService.getCustomerList(ownerId)); return "customer/list"; } }

这段链路最值得琢磨的地方是 Service 层那条业务规则:管理员和业务员看到的客户范围不同。很多毕设源码的列表查询是直接查出全部数据的,答辩时老师问一句"业务员怎么能看到别人的客户",答不上来就露怯了。加了这层判断之后,系统权限逻辑就有了,代码也谈不上复杂。MyBatis 的<where>标签会自动处理条件拼接,ownerId 传 0 时不会生成无效的 where 条件,这是比手动拼接 SQL 更安全的写法。

3.4 用 Postman 或浏览器验证接口:看返回视图还是 JSON、状态码 200 不代表业务成功

代码写完怎么验证?学到的第一个技巧是分清楚"请求到达了"和"业务成功了"是两回事。启动 Tomcat 后,浏览器访问http://localhost:8080/crm/customer/list,如果看到的是客户列表页面,说明链路是通的。这时候优先用 Postman 做几个关键测试:第一,未登录状态下直接访问这个地址,应该被拦截器重定向到登录页——这验证登录拦截是否生效;第二,用一个业务员账号登录后访问列表,数据里不应该包含 owner_id 不是你自己的客户;第三,故意传一个不存在的 ownerId 值,看是否报 500 错误,这能暴露 SQL 或空指针问题。

一个容易忽略的细节是:如果 Controller 方法返回值是 String 且工程配置了视图解析器,那么返回的 "customer/list" 会被解析成/WEB-INF/jsp/customer/list.jsp。如果访问时出现 404,先确认 JSP 文件是不是放对了目录。相反,如果源码里有 @ResponseBody 注解,那返回的就是 JSON 数据,一般用于 Ajax 异步刷新页面,两者别搞混了。

4. 数据库脚本怎么导入与验证:MySQL 命令行和 Navicat 两种方式、初始化数据的作用

4.1 拿到 .sql 脚本后先做三件事:看编码、看库名、看数据量

数据库脚本是整个项目的"地基文件",很多同学上来就用 Navicat 双击运行,跑出一堆红字就慌了。按照血泪经验,正确顺序是先打开脚本文件看三样东西。

第一看文件编码,用记事本或 IDEA 打开右下角显示的是 UTF-8 还是 GBK。脚本里如果有中文注释或初始化数据,编码不对会导致导入后中文全部变成乱码,后面页面显示中文肯定出问题。第二看库名,脚本里一般会有 CREATE DATABASE 语句,确认你要建的库叫 crm_db 还是别的,如果跟 jdbc.properties 里的 URL 不一致,连接必然报错。第三看数据量,脚本有多大数据量决定导入耗时和导入方式——几十行的初始化数据直接运行就好,好几万条模拟数据就需要命令行导入,图形界面容易超时。

4.2 命令行导入脚本:mysql -u root -p 的完整操作与参数说明,超时问题怎么处理

命令行导入是最常见也最可靠的方式,适合任何大小的脚本。Windows 上打开 cmd 或 Git Bash,进入 MySQL 的 bin 目录后执行:

mysql -u root -p < /path/to/crm_db.sql

按提示输入 root 密码后,MySQL 会逐条执行脚本里的 SQL 语句,执行成功时没有任何回显,有报错才会在终端打出来。这个命令的原理是:-u指定用户、-p表示需要密码、<把文件内容重定向给 mysql 客户端执行。注意路径中如果有中文或空格,用双引号包住整个路径,否则会报找不到文件。

如果你的脚本是几十 MB 的大文件,命令行导入可能卡住或报 lost connection 错误。解决方法是调整 MySQL 的 timeout 参数,导入前先在当前会话里执行一句:

SET SESSION wait_timeout = 600; SET SESSION max_allowed_packet = 128 * 1024 * 1024;

这两条命令把会话级的等待超时改成 600 秒,把最大允许的数据包调大到 128MB,大部分超时问题都能解决。注意 SESSION 级别的设置只对当前连接生效,不影响 MySQL 全局配置。

4.3 用 Navicat 导入的两种路径:直接运行 SQL 文件 vs 新建数据库后导入

Navicat 导入的操作更直观,但有几个典型问题容易被忽视。第一种方式是直接右键数据库选择"运行 SQL 文件",然后选中你的 .sql 脚本。这种方式要求你的连接已经有对应的数据库存在,如果脚本里有 CREATE DATABASE 语句且你连接的账号有建库权限,也可以直接运行整个脚本。第二种方式是自己先手工新建一个数据库,字符集选 utf8mb4,然后右键这个新库选择"运行 SQL 文件"。我推荐第二种,因为库是自己建的,后续排查比较不容易搞混。

Navicat 导入时有一个常见坑:脚本文件如果太大,Navicat 默认的读取缓冲区可能不够,会报"out of memory"或"invalid SQL syntax"。解决办法是用命令行导入,或者把脚本拆成小块再导。另外导入完成后务必刷新一下数据库列表,看表是否真的建出来了,有时候 Navicat 缓存没刷新,会误以为导入失败。

4.4 初始化数据的作用:一条管理员账号怎么写、admin/123456 这种默认账号背后的加密逻辑

脚本里的 INSERT 语句一般是整个数据库脚本最容易看明白的部分,但也别跳过。它通常包含两类数据:一是字典数据,比如客户等级的定义、行业分类的选项;二是测试账号,一般会有一个管理员账号和一两个业务员账号。

以管理员账号为例,脚本里往往是这样一条记录:

INSERT INTO sys_user (username, password, real_name, role_id, status) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员', 1, 1);

密码那一串是"123456"的 MD5 加密结果。为什么初始化数据存的是密文而不是明文?因为登录接口的校验逻辑是先把用户输入的密码用同样的算法加密,再跟库里比对——如果库里存的是明文,那加了密也白加。你启动系统后不管是用 admin/123456 还是 admin/admin 登录,都取决于源码里登录校验用的是什么加密算法,MD5 是绝大多数 SSM 项目的默认选择,个别会加一个固定盐值做二次混淆。你要是想改登录密码,不要直接在数据库里改,翻工具类里有没有 PasswordUtil 之类的加密方法跑一下,否则改了也登录不上。

4.5 验证表的关联关系:owner_id、customer_id 这些外键字段怎么检查是否对齐

表导入完之后,别急着启动项目,花五分钟检查外键逻辑是否对齐。检查方式就是执行几条 SQL:

-- 检查每个客户的归属人是否都存在于用户表 SELECT c.customer_name, c.owner_id, u.username FROM customer c LEFT JOIN sys_user u ON c.owner_id = u.id WHERE u.id IS NULL;

如果这条 SQL 查出了结果,说明存在"孤儿客户"——客户指向了一个不存在的用户。这种脏数据会导致页面在显示归属人姓名时 NPE 报错。同理,可以用类似的方式检查 follow_record 表的 customer_id 是否都能在 customer 表里找到对应记录。为什么要做这个检查?因为很多网上下载的源码脚本里,作者为了模拟数据随便写了一些 ID,没有严格对齐外键。你不在导入阶段排查,后面列表页或者详情页一旦做关联查询,就会在答辩演示时当场翻车。

5. SSM 客户资料管理系统常见问题与避坑:从 404 到中文乱码的 5 个血泪排查点

5.1 启动 Tomcat 后访问项目直接 404:web.xml 里 DispatcherServlet 映射路径在作怪

现象:项目能正常编译启动,Tomcat 也显示部署成功,但浏览器访问http://localhost:8080/项目名/返回 404。

原因:这个现象十有八九是 web.xml 里 DispatcherServlet 的<url-pattern>配置不对。很多源码用的是*.do这种后缀匹配,那你访问/customer/list这种不带 .do 的地址就必然 404,正确地址应该是/customer/list.do。反过来,如果配置的是/,那你起的地址就得是/customer/list。还有一种情况是 web.xml 里 load-on-startup 配置导致 Spring 容器没有初始化成功,ContextLoaderListener 报错,这种一般会在 Tomcat 日志里找到 ClassNotFoundException 之类的线索。

解决:先看 web.xml 的 url-pattern 是哪种风格,再决定访问用什么路径。如果要排查,最快的办法是打开 Tomcat 的 catalina.out 或 IDEA 控制台日志,拉到最底部看有没有Deployed字样,然后把报错的关键行复制下来搜一搜。

5.2 页面能打开但 JS/CSS 全部失效:静态资源被 SpringMVC 拦截的经典症状

现象:页面长得奇丑无比,完全没有样式,控制台报一堆 404,路径指向 /static/css 之类的静态资源。

原因:这是 SpringMVC 配置里的经典问题。当 DispatcherServlet 的 url-pattern 配置为/时,它会接管所有请求,包括静态资源,如果 spring-mvc.xml 里没有配置<mvc:resources>映射,那静态资源就全部找不到了。而如果用*.do后缀匹配,反而不会有这个问题,因为静态资源的路径不带 .do 后缀,不会被 DispatcherServlet 拦走。

解决:在 spring-mvc.xml 里加上静态资源映射:

<mvc:resources mapping="/static/**" location="/static/" />

如果你不想让静态资源路径里带 /static 前缀,也可以改成<mvc:annotation-driven />结合<mvc:default-servlet-handler />,让容器默认的 Servlet 来处理静态文件。这两种方案的差别是:前者你可以在 JSP 里按/static/css/style.css引用,后者你直接按/css/style.css引用就行,看源码里原来是怎么引用的,别改了引用路径反而找不到。

5.3 新增客户时中文写入数据库变成问号:连接参数和表字符集双重检查

现象:页面上填写的客户名称是"某某科技有限公司",保存成功后去数据库看,变成了一串问号。

原因:这个问题的根源是编码链路上的某个环节用了 latin1 或 GBK。最常见的因素有三处:数据库表的字符集不是 utf8mb4、JDBC 连接 URL 里没带 characterEncoding=utf8、JSP 页面本身的 contentType 没设置 UTF-8。三处断一环,中文就会在不同环节被转成错误编码。

解决:先执行一条 SQL 检查表的字符集:

SHOW TABLE STATUS LIKE 'customer';

看到 Collation 不是 utf8mb4_* 开头,用这条命令改掉:

ALTER TABLE customer CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

然后检查 jdbc.properties 的 URL 是否带 characterEncoding=utf8,最后在 web.xml 里配置 Spring 的 CharacterEncodingFilter,把所有请求和响应的编码强制成 UTF-8。这三个动作做完,中文乱码问题基本就绝迹了。

5.4 登录成功后跳转不进去、一直回到登录页:Session 存值 key 没对上

现象:账号密码都正确,登录接口也提示成功,但跳转到首页后马上被拦截器重定向回 login 页面。

原因:说明登录拦截器认为你没有登录。常见的坑是登录成功后往 Session 里放的 key 是user,而拦截器里取的是loginUser或者userId,对不上自然判定未登录。还有一种是拦截器取了 session 的值后判断方式是== null,但你存进去的是一个 Integer 类型的 0 值,不判空直接判等,逻辑就会出问题。

解决:打开 Interceptor 类和登录 Controller,找出 setAttribute 和 getAttribute 的 key 到底是什么——这种肉眼比对往往一眼就能看出不一致。还有一个隐蔽问题是 Map 和 Model 的使用混淆:SpringMVC 中往 session 里存值要用session.setAttribute(),而在 Controller 方法参数里加 HttpSession 再调用,别直接在 Model 上 setAttribute 以为能存进 session,那是两码事。

5.5 分页查询翻到第二页数据为空:MyBatis 分页参数没有持续传递

现象:第一页显示 10 条客户数据,点击下一页后页面空白或者数据缺失,地址栏里的 pageNum 参数变了但结果没变。

原因:翻页时查询条件丢失是最普遍的现象。第一次请求带了 keyword 和 pageNum 两个参数,第二次点击只传了 pageNum 没传 keyword,导致第二页的数据是在完全不同的查询条件下查出来的。如果你用的是 PageHelper 这类分页插件,还有个特有的坑:PageHelper 的分页参数是绑定在当前线程的 ThreadLocal 上的,如果查询前有一步走了缓存或者执行了别的 SQL,分页参数可能被"消费"掉。

解决:查看分页的 SQL 是不是在 Service 层里紧跟第一条查询——PageHelper 只对第一条紧接着的查询生效。翻页时要在 Controller 方法里把 keyword 参数继续传给前端模板,用${keyword}拼在分页链接的请求参数里。答辩演示时翻页功能是高频操作,这个不修好基本上演示必翻车。

6. 把客户管理系统做出差异化:Excel 导入导出、登录日志与验收前的验证清单

6.1 给客户资料加一个 Excel 导入导出:用 POI 封装一个工具类的实操

答辩评分里"功能完整性"占相当比重,而"客户资料批量导入导出"是很容易出彩又不难实现的功能。常见做法是用 Apache POI 库操作 Excel 文件,在 pom.xml 里引入 poi 和 poi-ooxml 两个依赖,然后自己封装一个工具类。核心代码大概是这样的:

// 导出客户列表为 Excel .xls 格式 public static void exportCustomerList(List<Customer> list, HttpServletResponse response) { HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("客户列表"); // 创建表头 String[] headers = {"客户名称", "联系人", "电话", "行业", "状态"}; HSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 填充数据 for (int i = 0; i < list.size(); i++) { HSSFRow row = sheet.createRow(i + 1); Customer c = list.get(i); row.createCell(0).setCellValue(c.getCustomerName()); row.createCell(1).setCellValue(c.getContactPerson()); row.createCell(2).setCellValue(c.getContactPhone()); row.createCell(3).setCellValue(c.getIndustry()); row.createCell(4).setCellValue(c.getStatus() == 2 ? "已成交" : "跟进中"); } // 设置响应头,让浏览器以文件下载方式打开 response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment; filename=customer_list.xls"); workbook.write(response.getOutputStream()); workbook.close(); }

这段代码的注意点有两个:第一,HSSFWorkbook 对应的是 Excel 97-2003 的 .xls 格式,数据量超过 65535 行会爆掉,毕设系统客户量一般到不了这个数,所以够用;要是想支持新版 .xlsx 格式,得上 XSSFWorkbook,但代码基本一样。第二,Content-Disposition 里的 filename 如果是中文,某些浏览器会乱码,最好用 URLEncoder 编码一下。导入功能就是反向操作:用 WorkbookFactory 创建一个 workbook,遍历每个 Sheet 的行和单元格,把数据封装成 Customer 对象然后批量插入,注意处理格式异常和空行的跳过逻辑。

6.2 增加一张登录日志表:记录用户名、登录时间、IP 地址,让你的项目多一个加分项

答辩时评委最常问的问题之一是"你做了哪些数据层面的设计"?如果你只有一个单表的增删改查,聊起来会很单薄。加一张登录日志表是个低成本高回报的选择:在用户登录成功的 Controller 方法里,把用户名、登录时间写进一张 login_log 表,管理员页面做一次最近登录记录列表展示即可。

具体的操作是:在登录方法里获取 HttpServletRequest 的 remoteAddr 得到客户端 IP,然后调用 LoginLogService 插入一条记录。表结构大概就是 id、username、login_time、ip_address、login_status 五个字段。这个功能的业务价值非常直观——能看到"谁在什么时候登录过系统",这个场景在答辩陈述时可以自然讲出来:系统上线后如果有人恶意尝试登录,管理员可以通过日志发现问题。不用做得多复杂,两张表、两个方法、一个列表页就成型了。

6.3 答辩前必做的功能验证清单:登录拦截、权限边界、空数据页面、重复提交

最后给你一份自查清单,每个都是踩过的坑,建议一项项验证完再去答辩。

第一项是未登录拦截:退出登录后,直接在浏览器输入/customer/list地址,应该被弹回登录页。第二项是业务员权限:用业务员账号登录后,把你的 owner_id 改成别人的 ID,再查列表,看是不是只能看到自己的数据;数据库里单独插一条 owner_id 为别人的数据,页面上不应该显示。第三项是空数据处理:把某张表 truncate 清空后访问对应页面,页面应该展示"暂无数据"而不是白屏或报错。第四项是重复提交:快速连续点两下新增按钮,数据库里不应该出现两条一模一样的客户记录,这个可以用前端按钮 disable 解决,也可以在 Service 层根据客户名加唯一校验。

6.4 一个实效技巧:把常用 SQL 的执行计划看一下,答辩时能多说两句优化

很多人的项目接口在数据量小的时候秒开,但答辩被问到"如果客户数据量上百万怎么优化"就卡壳。提前做一步准备:在数据库执行一条EXPLAIN SELECT * FROM customer WHERE owner_id = 1就能看到 MySQL 是怎么查的,如果 type 列是 ALL,说明是全表扫描,你就可以回答"可以通过给 owner_id 建索引优化"。做这一步不需要真的建索引,你只需要能说出来这个思路。同理,MyBatis 里尽量用 resultType 而不是循环查单条,也可以在综合查询的场景下自然带出。

这套项目做完,你会发现 SSM 真正的价值不在于技术多新,而在于它逼你把分层的思路理清楚。我的个人习惯是:每拿到一套陌生源码,先跑通最小链路,再逐层注释梳理逻辑,最后留出两天时间专门做权限和编码层面的加固。望这个方案能帮你把毕设做扎实,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 15:19:22

Zemax在眼科镜片设计中的核心技术与应用实践

1. 眼科镜片设计的技术背景与行业需求 近视、远视、散光等屈光不正问题已成为全球性健康挑战。世界卫生组织数据显示&#xff0c;全球约26亿人存在不同程度的视力缺陷&#xff0c;其中青少年近视率在东亚地区已突破80%。这一现状催生了庞大的视力矫正市场需求&#xff0c;而光学…

作者头像 李华
网站建设 2026/10/27 15:18:39

YOLO模型性能优化:从压缩到部署全流程解析

1. YOLO模型性能优化全链路解析YOLO(You Only Look Once)作为当前最流行的实时目标检测算法&#xff0c;在工业质检、自动驾驶、安防监控等领域广泛应用。但在实际部署中&#xff0c;我们常遇到模型体积过大、推理速度不足、硬件适配困难等痛点。本文将基于我参与的多个工业级部…

作者头像 李华
网站建设 2026/10/27 15:17:22

Linux三剑客find/tar/scp命令详解与实战技巧

1. Linux三剑客&#xff1a;find/tar/scp命令深度解析作为Linux系统管理员最常用的三个命令&#xff0c;find、tar和scp构成了日常工作的基础工具链。我在十五年的运维生涯中&#xff0c;这三个命令的使用频率高达日均20次以上。它们看似简单&#xff0c;但隐藏着大量高效用法和…

作者头像 李华
网站建设 2026/10/27 15:17:22

医学影像合成技术:从多模态融合到GAN应用

1. 医学影像合成技术概述医学影像合成是近年来医学图像处理领域的重要研究方向&#xff0c;它通过计算机算法将不同模态的医学图像进行融合或生成新的影像数据。这项技术在临床诊断、手术规划、医学教育等多个环节都展现出巨大价值。作为一名从事医学影像分析多年的工程师&…

作者头像 李华
网站建设 2026/10/27 15:16:08

曲线拟合:图像处理算法的底层基石与工程实践

曲线拟合这四个字&#xff0c;乍一听像是数值分析课本里的老古董&#xff0c;跟现在动辄上亿参数的视觉大模型比起来&#xff0c;显得特别"朴素"。但我做了这么多年图像处理相关的项目&#xff0c;越来越有一个体会&#xff1a; 真正决定一个视觉算法能不能落地的&a…

作者头像 李华
网站建设 2026/10/27 15:15:10

React调度器与最小二叉堆原理解析

1. 为什么React需要Scheduler调度器在React的架构演进过程中&#xff0c;调度器&#xff08;Scheduler&#xff09;的引入是一个关键的转折点。要理解最小二叉堆在其中的作用&#xff0c;我们需要先搞清楚React为什么要设计这样一个调度系统。React的核心任务是高效地更新UI。在…

作者头像 李华