简介这套基于SSMMySQL的图书管理系统设计与实现源码包定位为JavaWeb课程设计/期末大作业的满分参考方案面向需要快速完成图书管理类项目的在校生与开发者。内含完整Java源码、JSP页面、SQL数据库脚本及实验报告代码注释清晰配合前端静态资源CSS/JS/图片等与配置文件简单部署即可运行功能覆盖图书借还、分类管理、读者管理等常见模块。包内共664个文件压缩包约38.28MB以Java源码93个、JSP页面51个、图片素材124个PNG、106个GIF等为主体还包含CSS样式、SQL脚本和项目说明文档结构直观便于对照学习。当前已有357人学习与下载。借助这套完整方案读者可以迅速理解SSM整合原理与页面交互逻辑节省从零搭建的时间实验报告也为答辩或文档撰写提供了参考是冲刺高分、巩固JavaWeb技能的实用资源。1. 图书管理系统大作业为什么都选 SSMMySQL 这套组合图书管理系统大概是高校 Java Web 课程里出现频率最高的一类大作业而 SSMSpring SpringMVC MyBatis MySQL 又是其中最主流的组合。市面上的满分大作业源码包十有八九是这套技术栈SSM 撑起后端骨架MySQL 负责数据底座源码、数据库脚本和实验报告三件套各司其职。这套组合被反复选用的逻辑很清楚分层清晰Controller、Service、Mapper 各管一段写进实验报告就是现成的架构图Spring 的依赖注入配合 MyBatis 的半自动 SQL代码量保持在课程设计该有的体量——比 Spring Boot 全家桶更能展示设计过程比纯 JDBC 又体面得多。这篇文章适合正在做或准备做图书管理系统大作业的读者。无论你是打算从头自己写还是拿到一份参考源码想彻底搞懂它怎么跑起来、每个文件干什么沿着下面几个章节都能把项目吃透并把答辩时最常踩的坑提前填平。2. 数据库设计先行四张核心表的字段、外键与初始化 SQL拿到一套图书管理系统源码我先看的往往不是 Java 文件而是 SQL 脚本。表结构决定了一个课程设计的业务上限——字段设计得好后面写代码、画 ER 图、答追问都顺字段设计得含糊Controller 写得再漂亮演示时也经不起深挖。下面这套表结构是这类大作业的标准方案建议先照这个思路把库建出来再谈代码。2.1 功能模块与角色划分先定边界再建表动手建表之前把功能清单列出来划清两个角色的边界。管理员要做的是维护图书增删改查、上下架、维护分类、管理读者冻结与解冻、查看借阅记录和逾期罚款统计。读者要做的是检索图书、借书、还书、查看个人借阅历史和当前在借列表。我一般建议把借书-还书-逾期罚款这条完整链路做扎实其他功能点到为止。课程设计的评分逻辑通常是有一个完整闭环再有一个能展开讲的亮点而不是菜单铺了一大堆却没有一个功能能讲清楚。借书时校验库存并扣减、还书时自动计算逾期天数与罚款、管理端能看到热门图书排行这三个点任意一个都够在答辩时讲上五分钟。角色设计方面很多同学喜欢拆 admin 和 reader 两张用户表。我不推荐这么干这个项目里两类用户字段差异其实只有一两个管理员不需要学号读者不需要管理权限用一个 role 字段0 管理员 / 1 读者完全够用。合并之后登录只查一张表Session 里存 userId 和 role 就能做权限控制联表查询少一半实验报告里画 E-R 图也更省事。2.2 从用户到借阅记录tb_user、tb_book、tb_borrow 的关键字段设计四张核心表分别是tb_user用户、tb_category分类、tb_book图书、tb_borrow借阅记录。外表看起来简单但有几个字段要特别想清楚。tb_user 的 password 字段长度给到 VARCHAR(64)不是拍脑袋。MD5 摘要固定 32 位十六进制VARCHAR(32) 刚好够但一旦你想加盐或者升级成 SHA-25632 就卡脖子了64 留了余量。status 字段做冻结功能演示时可以现场把某个读者账号冻结再登录给他看账号已被冻结的提示这个细节很多同学忽略但它是管理端有完整语义的直接证据。tb_book 里最容易混的是 stock 和 borrow_count。stock 是当前可借库存每次借书减 1、还书加 1borrow_count 是累计借出次数只增不减用来做热门图书统计。两个字段表达两个完全不同的业务概念别合并成一个否则你还书时都不知道该加还是该减。tb_borrow 的状态字段建议用 1-借出中、2-已归还、3-逾期未还。逾期状态不要靠定时任务去改而是在还书那一刻实时判断还书时间晚于应还时间就按逾期天数计算罚款并置为 3。这个设计答辩时讲出来很加分因为它说明你想清楚了这个数据到底在什么时机被修改。2.3 初始化 SQL 的三个落地细节外键、索引与测试数据完整的建库脚本如下MySQL 5.7 和 8.0 都能直接跑。建库时指定 utf8mb4 是硬要求否则后面中文乱码会让你查找一整晚。-- 建库字符集统一 utf8mb4乱码问题从这里就掐断 CREATE DATABASE IF NOT EXISTS book_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE book_manager; -- 用户表管理员和读者共用一张表用 role 区分 CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码建议 MD5 后存储, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, student_no VARCHAR(20) DEFAULT NULL COMMENT 学号读者专用, phone VARCHAR(20) DEFAULT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT 0-管理员1-读者, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常0-冻结, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_role (role) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 分类表 CREATE TABLE tb_category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL, description VARCHAR(200) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书分类表; -- 图书表 CREATE TABLE tb_book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, isbn VARCHAR(20) NOT NULL, author VARCHAR(50) DEFAULT NULL, publisher VARCHAR(100) DEFAULT NULL, category_id INT DEFAULT NULL, price DECIMAL(8,2) DEFAULT NULL, stock INT NOT NULL DEFAULT 0 COMMENT 当前可借库存, borrow_count INT NOT NULL DEFAULT 0 COMMENT 累计借出次数, location VARCHAR(100) DEFAULT NULL COMMENT 馆藏位置, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-可借0-下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id), KEY idx_isbn (isbn), CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES tb_category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 借阅记录表 CREATE TABLE tb_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 借出时间, due_time DATETIME NOT NULL COMMENT 应还时间, return_time DATETIME DEFAULT NULL COMMENT 实际归还时间, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-借出中2-已归还3-逾期未还, fine DECIMAL(8,2) NOT NULL DEFAULT 0 COMMENT 逾期罚款金额, KEY idx_user (user_id), KEY idx_book (book_id), KEY idx_status (status), CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES tb_user(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES tb_book(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表; -- 初始数据一个管理员 两个读者 INSERT INTO tb_user (username, password, real_name, student_no, phone, role) VALUES (admin, 21232f297a57a5a743894a0e4a801fc3, 系统管理员, NULL, NULL, 0), (stu001, e10adc3949ba59abbe56e057f20f883e, 张三, 2023001, 13800000001, 1), (stu002, e10adc3949ba59abbe56e057f20f883e, 李四, 2023002, 13800000002, 1); -- 注e10adc3949ba59abbe56e057f20f883e 是 123456 的 MD5admin 那串是 admin123 的 MD5 INSERT INTO tb_category (category_name, description) VALUES (计算机, 编程、算法、数据库相关), (文学, 小说、散文、诗歌), (历史, 中外历史著作);细节一外键约束。删除一本图书前必须先确认 tb_borrow 里没有这本书的未归还记录否则外键直接报错。演示现场删不掉时第一反应应该是查借阅表而不是怀疑 SQL 写错。如果不想被这个约束打扰可以把两张外键改成语义上的逻辑关联但我建议保留因为实验报告里写通过外键保证数据完整性是一句实打实的话。细节二索引。isbn 用普通索引就够了不要设唯一索引——同一个 ISBN 的书可能有多本副本只是 id 不同。外键字段category_id、user_id、book_id和经常作为查询条件的 status 都值得加索引这几条是借阅列表页和图书列表页查询频率最高的列。细节三测试数据。至少要准备一个管理员、两个读者、十本以上覆盖三个分类的图书再手工插入几条借阅记录其中故意留一条应还时间已经过期、但还没还的记录。这样演示逾期罚款时不需要现场等三十天直接还这一本就能看到罚款金额跳出来演示节奏会从容很多。3. 三层框架搭建与核心代码从配置文件到借书流程跑通数据库就位后下一步是把 SSM 三层搭起来。这一章不列全部源码只把工程骨架和一条借书链路讲透——借书这条路通了其余增删改查就是同一套写法的复制粘贴不会有本质区别。3.1 Maven 依赖与两个核心配置文件版本组合和 Bean 装配先给一组经过验证的版本搭配Spring 5.2.x、MyBatis 3.5.x、mybatis-spring 2.0.x、MySQL Connector/J 5.1.49配 MySQL 5.7或 8.0.x配 MySQL 8、Druid 1.2.x。这套组合在 JDK 8 下编译运行没有任何问题也是绝大多数参考源码的默认配置。版本锁定这件事千万别大意Spring 5 和 Spring 4 的包名差异、mybatis-spring 2.x 对 Spring 5 的强依赖任何一个错位都会让你在 Tomcat 启动阶段就翻车。properties spring.version5.2.22.RELEASE/spring.version mybatis.version3.5.6/mybatis.version mybatis-spring.version2.0.6/mybatis-spring.version druid.version1.2.8/druid.version /properties dependencies !-- Spring MVC 与事务 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 与 Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency !-- MySQL 驱动MySQL 5.7 用 5.1.49MySQL 8 换成 8.0.33 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency !-- 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version${druid.version}/version /dependency !-- JSP 标准标签库 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies依赖只是前提真正决定框架能不能转起来的是两个 XML 配置。applicationContext.xml 是 Spring 的根容器负责数据源、SqlSessionFactory、Mapper 扫描和事务spring-mvc.xml 是 Web 层容器只管 Controller 和视图解析。两个容器职责分开是 SSM 的经典约定别图省事全塞进一个文件。!-- applicationContext.xml根容器管 Service、Mapper、数据源、事务 -- context:component-scan base-packagecom.example.bms !-- Controller 交给 spring-mvc.xml 管这里要排除掉 -- context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property nameurl valuejdbc:mysql://localhost:3306/book_manager?useUnicodetrueamp;characterEncodingutf8amp;useSSLfalseamp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueroot/ property nameinitialSize value5/ property namemaxActive value50/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.bms.mapper/ /bean tx:annotation-driven/这里有几个参数必须说清楚。url 里的useUnicodetruecharacterEncodingutf8是中文不乱码的前提两个参数要成对出现serverTimezoneAsia/Shanghai在 MySQL 8 驱动下是必填项缺了它日期会整体偏移八小时useSSLfalse是为了关掉本地开发的 SSL 握手告警。MapperScannerConfigurer 的 basePackage 写的是 Mapper 接口所在的包扫描到接口后会自动生成代理对象你才能在 Service 里直接Autowired一个接口进去。!-- spring-mvc.xmlWeb 容器管 Controller 和视图 -- context:component-scan base-packagecom.example.bms.controller/ mvc:annotation-driven/ mvc:default-servlet-handler/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /beanmvc:annotation-driven这一行必不可少它负责注册 HandlerMapping 和 HandlerAdapter同时帮你把 JSON 转换、类型转换等默认组件配好。少了它Controller 里的RequestMapping根本不会被识别页面 404 都算轻的。InternalResourceViewResolver 的 prefix 指向/WEB-INF/views/意味着所有 JSP 都要放在这个目录下Controller 返回逻辑视图名后由它拼出真实路径。3.2 借书业务的完整链路Controller 到 Mapper 的关键代码借书是整张业务表里最能体现设计感的操作因为它在同一个事务里干了三件事校验图书状态、扣减库存、插入借阅记录。把这三件事的先后顺序和并发安全性想明白答辩就赢了一半。先看 ControllerController RequestMapping(/borrow) public class BorrowController { Autowired private BorrowService borrowService; RequestMapping(/add) public String borrow(Integer bookId, HttpSession session) { // 登录拦截器已经保证 session 里有 userId Integer userId (Integer) session.getAttribute(userId); try { borrowService.borrowBook(userId, bookId); } catch (RuntimeException e) { // 业务异常直接带到列表页提示不回跳转混淆页面 return redirect:/book/list?error e.getMessage(); } return redirect:/borrow/list; } }Controller 里不写任何业务判断它的职责只是取参数、调用 Service、决定跳转。用户名从 Session 里拿而不是让页面传这是防止越权的第一道防线——如果 borrow 接口接收一个 userId 参数那读者就可以随便替别人借书了。Service 接口的实现才是核心Service Transactional public class BorrowServiceImpl implements BorrowService { Autowired private BookMapper bookMapper; Autowired private BorrowMapper borrowMapper; Override public void borrowBook(Integer userId, Integer bookId) { // 先查图书状态下架的或者不存在的直接拒绝 Book book bookMapper.selectById(bookId); if (book null || book.getStatus() ! 1) { throw new RuntimeException(图书不存在或已下架); } // 关键点用条件 UPDATE 代替先查库存再扣减从根上避免并发超借 int rows bookMapper.decreaseStock(bookId); if (rows 0) { throw new RuntimeException(库存不足借书失败); } // 生成借阅记录默认借期 30 天 Borrow borrow new Borrow(); borrow.setUserId(userId); borrow.setBookId(bookId); borrow.setBorrowTime(new Date()); Calendar cal Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, 30); borrow.setDueTime(cal.getTime()); borrow.setStatus(1); borrowMapper.insert(borrow); } }decreaseStock返回影响行数这是整个方法里最值得在答辩时展开的点。两个读者同时点借书如果先select再update两人都能查到 stock1最后库存会变成 -1而把条件写进 UPDATE 的 WHERE 子里数据库行锁保证只有一个请求能成功另一个影响行数为 0。MyBatis 的对应 SQL 如下update iddecreaseStock UPDATE tb_book SET stock stock - 1, borrow_count borrow_count 1 WHERE id #{bookId} AND stock 0 /updateTransactional保证 insert 和 update 同生共死如果 insert 失败抛异常前面的扣库存也会回滚。但要注意只有 RuntimeException 才会触发 Spring 默认的回滚策略如果代码里把异常 catch 住吞掉了事务就形同虚设。3.3 登录拦截器演示时被追问的高频点没有登录拦截器的图书系统等于把管理员的增删改查接口裸奔在公网上。课程设计阶段很多同学只在前端用session判断后端的 Controller 没有任何保护答辩老师登录一个普通读者账号直接访问/book/delete?id1系统照删不误——这是最容易翻车的场景。标准做法是加一个 HandlerInterceptorpublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); // 登录成功后把用户对象放进了 session这里只做存在性判断 Object loginUser session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }然后在 spring-mvc.xml 里注册并显式排除登录相关的路径和静态资源mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/login.jsp/ mvc:exclude-mapping path/static/**/ bean classcom.example.bms.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors/login和静态资源必须排除否则登录页面的 CSS 加载不出来、登录请求自己也被拦截直接死循环。如果你还细分了管理员和读者权限可以再写一个 role 判断loginUser.role 0才放行/admin/**路径。这个拦截器加上之后实验报告里的系统安全设计章节就有着落了。4. 连接、事务、分页与日期四个必调参数和页面联调细节代码能编译只是第一步。很多源码包导入后你第一眼看到的不是功能页面而是一串参数报错。这一章把演示前必须检查的四个参数点讲清楚它们几乎决定了你的系统是能跑还是能演示。4.1 MySQL 连接串与 Druid 连接池的必调参数连接串是排查乱码、日期偏移、连不上库的第一现场。把这几个参数背下来遇到问题先对照一遍连接串参数作用必填场景useUnicodetruecharacterEncodingutf8驱动与数据库之间以 UTF-8 传输只要表里有中文就必填useSSLfalse关闭 SSL 握手本地开发必填否则日志全是警告serverTimezoneAsia/Shanghai指定驱动使用的服务器时区MySQL 8 驱动必填否则日期偏差 8 小时allowPublicKeyRetrievaltrue允许获取公钥用于密码认证MySQL 8 配合 caching_sha2_password 时可能用到Druid 连接池的参数里我建议至少设置这四个其他用默认值就够课程设计用参数建议值说明initialSize5启动时建立 5 个连接页面首访不卡maxActive50最大活跃连接数课程设计 50 足够maxWait10000拿不到连接时最多等 10 秒避免页面无限转圈validationQuerySELECT 1空闲连接检测防止 MySQL 8 小时超时断开连接maxWait 是很多人忽视的救命参数。没有它当连接池被打满时新请求会一直阻塞Tomcat 线程被耗尽页面表现为假死——刷新没用重启才行。设成 10 秒后超时直接抛异常你能在日志里看到明确错误而不是一无所知。提示本地演示前把 url 里的密码改成你自己的 MySQL 密码这是所有数据库连不上类问题的第一排查项。4.2 事务与分页课程设计里最容易被追问的两个点事务上面已经提过这里补两个容易栽的细节。第一Transactional只能加在 public 方法上加到 private 方法上 Spring 的代理根本看不见静默失效。第二Spring 默认只在 RuntimeException 时回滚如果你在 catch 里把异常吞掉再返回一个正常值事务同样白配。养成习惯业务方法里不要 catch让异常往上层抛由 Controller 统一处理提示。分页是借阅记录列表和图书列表的标配。课程设计阶段有两种做法自己拼LIMIT或者用 PageHelper 插件。我更推荐 PageHelper因为它把总条数统计和当前页数据一次搞定少写不少样板代码。在 SqlSessionFactory 里注册插件bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameplugins array bean classcom.github.pagehelper.PageInterceptor property nameproperties value helperDialectmysql reasonabletrue /value /property /bean /array /property /beanService 里使用时有个经典坑PageHelper.startPage()必须紧跟它的第一条 MyBatis 查询中间不能插入其他查询语句否则分页会被应用到错误的 SQL 上。常见写法是PageHelper.startPage(pageNum, pageSize); ListBook list bookMapper.selectByCondition(bookName, categoryId); PageInfoBook pageInfo new PageInfo(list);reasonabletrue的意思是页码越界时自动修正你请求第 99 页但总共只有 5 页它会把页码钳到第 5 页而不是返回空列表。这个细节在演示时很实用因为你不知道台下老师会翻到哪一页。4.3 JSP 页面的日期格式化和状态字段显示后端把Date对象塞进 request 交给 JSP页面直接输出会得到一长串英文格式比如Thu Sep 01 00:00:00 CST 2024既不美观也不像中文系统。统一用 JSTL 的 fmt 标签处理% taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % %-- 借阅时间 --% fmt:formatDate value${borrow.borrowTime} patternyyyy-MM-dd HH:mm:ss/ %-- 状态字段用 c:choose 转成中文别在 Java 里拼字符串 --% c:choose c:when test${borrow.status 1}借出中/c:when c:when test${borrow.status 2}已归还/c:when c:when test${borrow.status 3}逾期未还/c:when /c:choose状态字段的显示要集中处理别散落在各个 JSP 里各写一套。一个页面用 c:choose另一个页面直接输出数字 1答辩老师很容易看出你的代码是拼凑的。罚款金额也是同理${borrow.fine}直接输出可能不带两位小数用 fmt:formatNumber 统一成0.00格式。5. 避坑指南SSM 图书系统从启动到演示的 5 个常见问题下面这几条是这类项目里出现频率最高的问题每条我都按实际排查顺序写现象对得上就直接跳到解决步骤。5.1 依赖版本冲突导致 Tomcat 启动即失败现象Tomcat 启动时报java.lang.NoClassDefFoundError或BeanCreationException: Error creating bean with name sqlSessionFactory控制台日志指向org.springframework.beans.factory但代码本身看起来没有任何问题。原因最常见的有两类。一是 Spring 与 mybatis-spring 版本跨度太大mybatis-spring 2.x 要求 Spring 5.x如果你用的是 Spring 4.x启动时会出现 Bean 创建异常二是 IDEA 的 Artifacts 配置问题——pom 里明明有依赖但 Tomcat 部署的 war 包里没有把依赖打进WEB-INF/lib运行时自然找不到类。解决版本按本章 3.1 节的组合锁定统一用properties管理版本号然后在 IDEA 的 Project Structure → Artifacts 里确认 Output Layout 包含lib目录并勾选所有依赖。改完后 Clean 再 Rebuild别直接 Redeploy旧的 lib 会残留。5.2 MySQL 8 驱动改名与日期偏移现象配置文件和 pom 都没动启动后报ClassNotFoundException: com.mysql.jdbc.Driver或者系统能跑但所有日期字段都比真实时间晚了 8 小时。原因MySQL Connector/J 从 8.0 开始把驱动类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver同时强制要求连接串里带serverTimezone。你的源码包如果是在 MySQL 5.7 时代写的拿到 MySQL 8 环境必然踩这两个问题。解决二选一。如果继续用 MySQL 5.7驱动保持5.1.49不动如果本地装的是 MySQL 8把 driver-class 改成com.mysql.cj.jdbc.Driver并在 url 末尾加上serverTimezoneAsia/Shanghai。我一般建议跟随本地 MySQL 版本改驱动而不是给本地降级数据库。5.3 JSP 页面 EL 表达式原样输出或取不到数据现象页面上直接显示${book.bookName}这样的字符串或者表达式没报错但显示空白。更隐蔽的是列表页其他数据都正常只有某一列取不到值。原因前者通常是 web.xml 版本太低EL 默认被禁用或者 web.xml 里显式写了el-ignoredtrue/el-ignored后者九成是 request attribute 的名字没对上——Controller 里放进去的是bookList页面却写${books}两边差一个字母。解决确认 web.xml 用的是 Servlet 3.0 以上版本头看web-app标签的 version 属性并检查 attribute 名逐字比对。还要注意return redirect:/book/list是重定向request 域的 attribute 会全部丢失return /book/list是转发attribute 才保留。这个区别在调试时很容易让人怀疑人生。5.4 中文乱码的三处源头现象数据库里存进去的中文变成问号或者从数据库查出来在页面上显示乱码。有时插入正常、查询乱码有时反过来。原因乱码从来不是一个点的问题是链路里三处不一致。数据库/表字符集不是 utf8mb4连接串缺characterEncodingutf8JSP 页面pageEncoding不是 UTF-8。三处只要有一处不同就会在某一环节翻车。解决按顺序检查。SQL 脚本建库时指定DEFAULT CHARACTER SET utf8mb4url 里带useUnicodetruecharacterEncodingutf8JSP 头部写% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%。三处统一后把已经乱码的表 drop 重建一次再验证。5.5 Transactional 回滚不生效现象借书时插入借阅记录成功但库存扣减失败抛了异常前台的 insert 却没有回滚页面上出现有借阅记录但库存没减的数据不一致。原因三层原因按概率排——异常被 catch 住吞掉了Transactional加在了 private 方法上或者根容器没配置tx:annotation-driven。第一个最常见因为初学者习惯在 Service 里 try-catch 后 return 一个结果。解决业务方法里不要自己 catch抛出去让 Controller 统一处理Transactional必须加在 public 方法上检查 applicationContext.xml 里确实有tx:annotation-driven/并且SqlSessionFactory的事务管理器已被 Spring 管理。改完重启故意制造一次库存不足的借书观察数据库里是否多出半截数据没有就是回滚生效了。6. 让大作业从能跑变成能拿高分实验报告与答辩的加分技巧很多人的系统功能没问题最后分数却不理想问题出在实验报告和答辩表现。报告不是代码的堆砌而是让老师快速确认你确实理解这套系统的说明书。6.1 实验报告的结构图表比代码更值钱我建议按五个章节组织需求分析功能清单 用例描述、数据库设计E-R 图 表结构说明、系统设计分层架构图 模块职责、核心实现借书/还书流程的关键代码 时序说明、系统测试测试用例表 操作截图。图表数量尽量大于代码数量代码只贴关键流程的核心片段别把全部源码复制进去。数据库设计这一章把第二章的建表脚本整理成表结构说明表格每个字段写清楚含义外键关系用文字描述。系统测试部分至少要列十行测试用例正常登录、错误密码、冻结账号登录、借书成功、库存不足借书、还书逾期罚款、分页查询、模糊搜索。每行写好操作步骤、预期结果、实际结果这就是满分报告的样子。6.2 答辩现场最常被问的 4 个问题第一个问题几乎必问为什么用 SSM 而不是 Spring Boot标准答案不是踩一个捧一个而是说明 SSM 的分层让你能更清楚地展示 Controller、Service、Mapper 的职责边界MyBatis 手写 SQL 便于你控制复杂的联表查询。第二个问题并发情况下两个读者同时借最后一本书怎么办把 3.2 节的条件 UPDATE 讲出来再补一句数据库行锁保证只有一个请求能成功扣减这个回答可以直接让老师点头。第三个问题逾期罚款是怎么算的要讲清楚计算时机——还书那一刻实时计算用还书时间减去应还时间得到逾期天数再乘每日罚款单价而不是靠定时任务批量改状态。第四个问题这些索引为什么这么建回答案isbn 用于图书检索、外键字段用于联表、status 用于状态过滤查询频率最高所以加索引。我自己带项目时的习惯是拿到任何一份参考源码第一件事永远是先把 SQL 脚本从头读一遍再对着表结构看 Mapper 的 SQL。代码可以包装出复杂的假象但数据库设计骗不了人。这个习惯帮我避免过太多次答辩现场被问到底层设计时哑口无言的尴尬希望你也能用上。希望帮到你。本文还有配套的精品资源点击获取