多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SSM框架下的图书馆预约系统:从数据库设计到并发控制

SSM框架下的图书馆预约系统:从数据库设计到并发控制 1. 系统整体拆解图书馆预约系统的需求与设计思路1.1 为什么选这个题目以及它到底解决了什么问题每年毕业设计选题的时候图书馆预约管理系统总是一个高频选项。很多同学觉得它常规但恰恰是这种看似普通的题目反而最适合用来完整展示你对软件工程全流程的掌握程度。我见过太多选了智能推荐购物平台基于深度学习的某某系统这类大而全题目的同学最后由于工作量失控连基本的功能都没跑通反倒是踏踏实实做一个图书预约管理系统的人把需求分析、数据库设计、接口封装、权限控制全流程走了一遍答辩时讲得清清楚楚一次性通过。说回这个系统本身。图书馆预约管理系统的核心痛点很明确自习座位资源紧张、占座现象严重、管理员难以实时掌握座位使用情况。传统的排队和现场抢座方式效率低还容易引发矛盾。所以系统要解决的核心问题就是三件事让读者可以提前查看座位状态并预约让管理员可以统一管理座位和用户数据让违约行为有据可查、有规可惩。这背后其实就是一个标准的管理信息系统MIS的缩影麻雀虽小五脏俱全。涉及用户角色划分、资源状态管理、业务规则约束、数据统计查询。把这个系统的逻辑想透了以后再做库存管理、会议室预订、实验室排课这类系统你会发现都是同一套套路换汤不换药。这也是为什么这种题目在毕设中长盛不衰的真正原因——它足够典型能考察你完整的基本功。1.2 角色权限与业务流程全景先明确系统里有哪几类人每类人能做什么这直接决定了后台菜单怎么设计、接口怎么控制权限。这个系统我建议按三个角色来设计。**管理员图书馆工作人员**是权力最大的角色负责维护整个系统的运行。具体职责包括对用户账号进行审核和禁用、对阅览室和座位进行增删改查、查看所有预约记录、处理违约申诉、发布公告通知。注意管理员是管理而不是使用他本人不应该参与座位预约否则会造成管理者和使用者角色混淆。**注册用户学生/读者**是这个系统的主体用户。他们的核心操作链路是注册登录 → 浏览阅览室与座位 → 选择空闲座位发起预约 → 到馆后扫码或在终端签到 → 使用完毕释放座位。预约会涉及一个关键业务规则如果超过预约开始时间一定时长比如30分钟未签到系统会自动取消预约并记录一次违约累计违约达到一定次数比如3次该用户在未来一段时间内比如一周被限制预约。**游客未登录访问者**能做的事情非常有限只能查看公告和座位状态总览。想看详细座位图、发起预约必须登录。这么设计一方面是业务需要另一方面也天然形成了未登录状态下的权限拦截需求对应到技术上就是一个拦截器Interceptor或者过滤器Filter的事。业务流程上最核心的是预约链路我建议画一条主线出来用户选座 → 提交预约 → 系统校验座位是否空闲、用户是否有违约限制→ 生成预约记录 → 用户在预约时段内签到 → 使用完成后手动释放或到点自动释放 → 系统更新座位状态。整条链路的状态流转必须清晰因为状态字段设计得好不好直接影响后面写SQL的复杂度和代码的可维护性。1.3 SSM框架选型的真实理由以及为什么不选Spring Boot现在很多同学一上来就习惯用Spring Boot做新项目觉得配置简单、起步快。但作为毕业设计我反而推荐你选择SSMSpring MVC Spring MyBatis这种经典组合。这里面的考虑很多同学可能没细想过。第一SSM框架组合是过去十多年Java Web开发的主流方案大量的企业老项目至今还在用。你把这个组合吃透了去实习或者入职后接手老项目不会两眼一抹黑。第二SSM里大量使用XML配置和手动装配它能逼着你把Spring的IoC容器、AOP切面、SpringMVC的请求流转、MyBatis的SQL映射这些底层机制搞明白。使用Spring Boot的时候这些细节多数被自动配置掩盖了出了问题反而不容易排查。第三从答辩角度来看SSM项目的配置文件和代码结构能给评委提供更丰富的提问点你展示自己对配置逻辑的理解比展示脚手架自动生成的代码更有说服力。当然这里要说明白并不是说Spring Boot不好而是说在毕业设计这个场景下SSM的学习价值和展示价值更高。如果你的指导老师明确要求可以用Spring Boot那你当然可以迁移——SSM的核心思想分层、IoC、ORM在Spring Boot中照样适用。后面讲代码结构的时候你会发现Controller、Service、Mapper这种分层方式在两个框架下是完全一致的知识完全通用。2. 数据库设计与表结构详解2.1 五张核心表以及每张表的设计理由数据库设计是答辩时评委最容易深挖的部分也是整个系统数据流的地基。我的建议是不要贪多把核心业务表做扎实比堆砌十张没用的表强得多。这个系统的核心表我推荐如下五张外加一张公告表可选。用户表user字段包括用户ID主键、用户名、密码、真实姓名、学号/工号、手机号、角色1表示管理员0表示普通用户、状态0禁用1正常、注册时间、违约次数。密码字段我建议存储MD5加密后的密文而不是明文。这里多说一句真实项目中一般会用加盐加密比如BCrypt但毕设中用MD5加密已经能体现你的安全意识到位了答辩时你能说清楚为什么不能存明文就是加分项。阅览室表reading_room字段包括房间ID、房间名称、位置描述、可容纳座位总数、开放时间如08:00-22:00、状态。为什么需要单独一张阅览室表因为座位一定是归属于某个物理空间的没有这张表座位信息就会缺少归属维度系统后期的统计功能比如哪个阅览室使用率最高就做不出来。座位表seat字段包括座位ID、所属房间ID外键、座位编号如A-101、座位状态0空闲1已预约2已签到使用中3维修中。注意座位表里不应该存预约人是谁这种业务信息那应该由预约记录表来挂否则一个座位多人预约时数据就会乱。座位表只管物理状态这是数据库设计里单一职责的体现。预约记录表reservation这是整个系统业务逻辑最密集的表。字段包括预约ID、用户ID外键、座位ID外键、预约日期、开始时间段、结束时间段、预约创建时间、状态0已预约1已签到2已释放/完成3已取消4已违约、签到时间、取消时间。设计这张表的时候一定要想清楚预约的是日期时间段的组合所以时间字段建议分开存日期、开始时间、结束时间三个字段这样后续做查询某天某个时间段的空闲座位这种SQL时条件判断会更清爽。违约记录表violation字段包括违约ID、用户ID外键、关联的预约ID、违约类型如超时未签到、违约时间、处理状态。这张表的价值在于把违约行为从预约记录里剥离出来独立管理。这样管理员可以单独查询谁违约最多用户在个人中心也能看到自己的违约历史后续做累计3次禁用一周这种业务规则时直接查这张表统计就行非常方便。2.2 表关系与字段类型的关键细节表之间的关系很清晰用户与预约记录是1对多座位与预约记录是1对多阅览室与座位是1对多。一个用户可以有多个预约记录一个座位在不同时间段也可以被多个预约记录关联注意物理上座位只有一张但预约记录表里可以通过日期时间段字段来实现同一座位不同时段的多次预约。这里有一个非常容易踩坑的细节座位表里如果只保存当前状态0空闲/1预约/2使用中那么同一座位在一天的不同时间段是被谁预约的座位表自己根本不知道必须联合预约记录表才能回答这个问题。所以实现时座位表状态可以作为缓存字段方便列表展示但真正的业务判断比如9点到10点这个座位是否可约一定要去查预约记录表在代码里做好一致性维护。我在第一次做这个系统的时候就是因为图省事只查座位表状态结果出现了同一时间段被两个用户重复预约成功的bug后来才补上的这个逻辑希望你别再踩一遍。字段类型方面主键统一用自增的INT简单可控日期时间字段用DATETIME状态字段用TINYINT用户名、姓名这类短文本用VARCHAR(20-50)备注性字段用VARCHAR(255)就够。唯一索引方面建议在预约记录表上加一个联合唯一索引用户ID预约日期开始时间段防止同一个用户在同一时间段重复提交预约。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(20) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 存储MD5密文, real_name VARCHAR(20) DEFAULT NULL, student_no VARCHAR(20) DEFAULT NULL, phone VARCHAR(11) DEFAULT NULL, role TINYINT DEFAULT 0 COMMENT 0-普通用户, 1-管理员, status TINYINT DEFAULT 1 COMMENT 0-禁用, 1-正常, violation_count INT DEFAULT 0, register_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE seat ( id INT NOT NULL AUTO_INCREMENT, room_id INT NOT NULL, seat_no VARCHAR(20) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-空闲, 1-已预约, 2-使用中, 3-维修中, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.3 为什么建议加上时间段字段而不是只用具体时间点这是这个系统设计里我认为最值得展开的一个点。很多同学第一次做预约系统预约记录里只有一个预约时间比如2025-01-06 14:00:00签到也只有一个签到时间看起来好像也能跑通但你细想就知道问题大了图书馆一座位的使用是以小时为单位连续进行的比如上午8点到10点和上午9点到11点这两段预约在时间上是重叠冲突的9点到10点重叠。如果你的表结构里只有预约时间这个创建时间字段数据库根本无法判断这个冲突。我的做法是将预约抽象为日期时间槽模型。具体而言把一天按小时切成多个时间段也可以让管理员自定义时间段的粒度比如上午、下午、晚上三个大时段用户预约时选择某个日期下的某个时间段预约记录里保存的是日期DATE类型和开始、结束的TIME类型字段。判断座位是否可约就是查预约记录表里有没有同一天 时间段有交集 状态为已预约/已签到的记录。SELECT COUNT(*) FROM reservation WHERE seat_id #{seatId} AND res_date #{date} AND #{beginTime} end_time AND #{endTime} begin_time AND status IN (0, 1)这段SQL里两个条件#{beginTime} end_time AND #{endTime} begin_time就是标准的区间重叠判断逻辑。你在答辩的时候能把这个写出来然后解释一句两条预约在时间线上有交集就说明时间冲突评委基本就不会在这个点上再追问什么。至于时间段的粒度是半小时、一小时还是半天这是一个灵活配置项可以在系统管理里做一个时间粒度配置默认按小时切就行。3. 核心功能实现与关键代码解析3.1 登录与权限控制拦截器配置和密码加密登录功能是每个系统的门户也是最基础的能力展示点。我使用的方案是典型的Session 拦截器组合。用户登录成功后把用户对象放入Session定义一个字符常量如LOGIN_USER作为Session的Key。然后配置一个SpringMVC的拦截器对所有请求路径进行拦截检查未登录的用户一律跳到登录页。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(LOGIN_USER); if (user null) { // 未登录重定向到登录页 response.sendRedirect(request.getContextPath() /login); return false; } return true; } }然后在SpringMVC的配置文件中注册这个拦截器并配置拦截路径。这里有一个细节登录请求本身、注册请求、静态资源CSS/JS/图片必须排除在外否则就会死循环跳转。另外管理员专属接口和普通用户接口如果分开可以在拦截器里再判断角色或者再单独配一个管理员权限的拦截器。我建议用HandlerInterceptor配合路径规则搞定角色控制这种事少用注解因为SSM项目里注解方式虽然也有AuthRequired之类但写起来不如路径规则直观答辩也不好讲。密码加密方面我使用JDK自带的MessageDigest对密码做MD5处理后再入库。这里要注意编码问题MessageDigest.getInstance(MD5)后对字符串做getBytes(UTF-8)进行摘要运算再把结果转成十六进制字符串存储。每次登录验证时把用户输入的密码做同样处理再和数据库里的密文比较。哪怕答辩时被问到MD5有碰撞风险怎么办你只要能说真实项目会用加盐方案我这里出于课程设计演示目的用MD5已经比明文要安全这个回答就圆得过去。3.2 座位预约核心业务事务控制与状态校验预约是系统里业务逻辑最重的功能也最容易出并发问题。我先说最理想的完整流程再给出代码。第一步接收前端传来的座位ID、预约日期、时间段参数第二步做后台二次校验前端校验不可信查询座位是否存在、当前时间是否在可预约范围内、该用户是否有未解除的违约限制第三步通过SELECT ... FOR UPDATE锁定座位或预约记录查询这个时间段是否已被预约悲观锁方案第四步如果空闲插入预约记录同时把座位状态改为已预约第五步提交事务。这里我用了悲观锁原因是毕设场景下没必要上Redis分布式锁或者乐观锁版本号这种高并发方案FOR UPDATE足够简单且应对并发校验很有效。你也许会觉得用乐观锁更优雅一些但你要想清楚座位预约的核心矛盾是同一时间段只能被预约一次悲观锁直接在数据库层面锁行SQL逻辑直观答辩时你能把锁机制讲清楚这已经是亮点。Service public class ReservationServiceImpl implements ReservationService { Autowired private SeatMapper seatMapper; Autowired private ReservationMapper reservationMapper; Autowired private ViolationMapper violationMapper; Transactional(rollbackFor Exception.class) Override public boolean reserveSeat(Integer userId, Integer seatId, Date resDate, String beginTime, String endTime) { // 1. 校验座位存在且状态可用 Seat seat seatMapper.selectById(seatId); if (seat null || seat.getStatus() 3) { throw new BusinessException(座位不存在或处于维修中); } // 2. 查询冲突预约锁定相关记录防止并发重复预约 int conflictCount reservationMapper.selectConflictCount(seatId, resDate, beginTime, endTime); if (conflictCount 0) { throw new BusinessException(该时间段已被预约请选择其他时间); } // 3. 检查用户违约次数 User user userMapper.selectById(userId); if (user.getViolationCount() 3) { throw new BusinessException(违约次数已达上限暂时无法预约); } // 4. 插入预约记录 Reservation reservation new Reservation(); reservation.setUserId(userId); reservation.setSeatId(seatId); reservation.setResDate(resDate); reservation.setBeginTime(beginTime); reservation.setEndTime(endTime); reservation.setStatus(0); // 已预约 reservationMapper.insert(reservation); // 5. 更新座位状态为已预约 seatMapper.updateStatus(seatId, 1); return true; } }注意Transactional必须加在Service实现类的public方法上事务管理要确保预约记录插入和座位状态更新要么一起成功要么一起回滚。这里有个容易忽略的坑如果MySQL默认事务隔离级别是REPEATABLE_READ而且在冲突查询与插入之间没有行锁保护高并发下仍然可能出现幻读两个事务同时查到0冲突然后都插入成功。所以我还是建议在冲突查询SQL里加上FOR UPDATE锁定预约记录表的相关行或者锁定座位行形成先锁后查的完整保护。签到和释放的逻辑相对简单。签到时将预约记录状态从0改为1同时把座位状态从已预约改成使用中。释放时把预约记录状态改为2座位状态改回0。这两步操作同样放在一个事务里保持一致。至于预约起始时间自动判定违约可以用一个定时任务Spring的Scheduled每分钟扫描一次预约记录表把开始时间早于当前时间20分钟且状态仍为0的记录标记为违约。定时任务在少数不熟悉这个功能的毕设系统里是个加分项你可以自己加上。3.3 后台管理统计报表与状态维度管理后台管理模块的常见误区是堆表格。有些同学为了显得功能多把用户表、座位表、预约表全部原样展示出来结果看起来就是一个数据库管理界面没有任何信息提炼。我建议你做这几个有说服力的管理功能座位的批量增删支持通过房间号批量生成多个座位号、预约记录的条件筛选按日期、用户、状态组合查询、用户禁用/解禁、违约次数清零、按阅览室统计当日利用率。统计功能是答辩时很有展示价值的一块。你可以写一个简单的统计查询比如查询最近7天每天的预约总数。SELECT DATE_FORMAT(res_date, %Y-%m-%d) AS day, COUNT(*) AS total FROM reservation WHERE res_date DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND status IN (0, 1, 2) GROUP BY day ORDER BY day在后台界面可以用ECharts或Chart.js把这个统计结果画成折线图非常直观。ECharts在毕设中使用很普遍引入方式也很简单在JSP页面里script srcecharts.min.js/script然后写一个option对象用AJAX从后端取数据填充。别忘了告诉评委这不是硬编码的数据是从预约记录表实时统计出来的数据库里数据一变图表就跟着变。// 前端JSP页面中通过AJAX加载统计接口 $.ajax({ url: /admin/statistics, type: GET, dataType: json, success: function(result) { var chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: 近7日预约趋势 }, xAxis: { type: category, data: result.days }, yAxis: { type: value }, series: [{ type: line, data: result.counts }] }); } });实操时最常用的配置是采用前后端分离思路来做页面但毕设多数还是JSP JSTL。JSP的好处是服务端渲染简单结合c:forEach标签循环表格数据非常方便而且不用额外搭建前端工程。缺点是页面逻辑耦合较大但毕设够了。如果你对前端比较熟也可以把Vue和AJAX引进来页面更现代不过需要注意的是SSMJSP是最稳的方案不容易出跨域或者打包部署问题。3.4 公告管理、个人中心与辅助功能除了核心预约流程系统还需要几个辅助模块来保证完整性。公告管理是管理员发布通知的入口用户在首页和登录后能看到最新公告列表。个人中心展示用户的基本信息、预约历史进行中、已完成、已取消、违约记录并提供修改密码和退出登录功能。这些模块都是标准CRUD实现难度不大但要注意两点一是列表分页二是安全访问比如个人中心只能查自己ID的数据不能通过修改URL参数查别人的数据。我在第一次做的时候安全意识不够个人中心的数据查询居然没有限制用户ID被老师点出来越权访问的问题现在回想起来非常惭愧这个点你在设计和答办时也要主动留意。这里我想插一句对源码学习的看法。每年都有大量同学在这个阶段从网上下载各种SSM图书馆管理系统源码.zip然后导入IDE发现一堆报错。核心问题在于下载下来的项目版本五花八门有的用的Spring3有的是4有的是Maven项目有的是传统Web项目导入方式完全不同很多源码缺少说明文档数据库脚本和配置步骤都埋在压缩包深处没人告诉你一些源码是半成品看起来页面齐全但是底层事务、拦截器、异常处理都是空的。所以如果你准备参考开源源码或者购买的模板项目我强烈建议你至少花一个晚上的时间把下面第4节梳理的导入配置流程完整走一遍并且亲手把核心业务代码比如预约冲突校验、定时违约处理重写一遍。抄一遍代码解决的只是完成重写一遍解决的才是理解和答辩。4. 项目部署运行与源码使用指南4.1 开发环境选型与版本兼容建议这里是很多同学导入项目后第一个翻车现场。我先给出一套我亲测稳定、兼容性极好的环境组合你照着准备能省掉大量折腾时间。JDK版本JDK 1.8也叫8。不要用JDK 11、17、21因为老版本Tomcat和Spring对这些新JDK的支持和兼容存在很多坑而且很多教学资料默认JDK8。MySQL版本MySQL 5.7。8.0也行但要注意连接驱动版本和时区设置问题如果你不太熟悉优先5.7。Tomcat版本Tomcat 8.5或9.0。这两个版本兼容Servlet 3.1/4.0SSM项目运行没有任何问题。Maven版本Maven 3.6左右。注意Maven配置阿里云或华为云镜像仓库否则下载依赖依赖会慢到怀疑人生。IDEIDEA推荐或Eclipse。IDEA中导入Maven项目比较顺手Spring配置文件之间的跳转和XML校验也更智能。如果你的项目是用Maven管理的强烈建议用Mavenpom.xml里核心依赖版本大致是这样一套组合这几个版本搭配经过了大量项目验证冲突较少。properties spring.version5.2.12.RELEASE/spring.version mybatis.version3.5.6/mybatis.version mybatis-spring.version2.0.6/mybatis-spring.version /properties顺带提醒一下mybatis-spring版本要和mybatis、spring-jdbc的版本匹配否则启动会报MapperScannerConfigurer相关的诡异错误。这个错误本质上是版本之间接口签名变化导致的排查起来非常消耗时间最简单的方式就是锁定上面这组版本。4.2 导入项目到IDEA的完整步骤先把数据库脚本执行了。找到SQL脚本文件一般叫library_db.sql或init.sql用Navicat或命令行执行生成数据库和表结构。然后确认脚本里有没有预置测试数据如果没有建议手动插入几条测试账号数据一条管理员两条普通用户方便后续测试登录。顺便把密码的MD5值也准备好比如123456的MD5是e10adc3949ba59abbe56e057f20f883e你不会想在数据库里直接更新成明文密码再让系统读取那会导致登录永远失败。打开IDEA选择File - New - Project from Existing Sources选中项目的pom.xml文件按Import Maven Project向导走完。这里有个常见的坑如果项目不是Maven结构而是传统Web结构有.classpath但没pom.xml那就用File - Open直接打开整个目录然后在Project Structure里把编译输出目录、Web Facet、Artifacts手动配好。两种方式的差别在于你拿到的源码是哪一种结构。我的建议是如果你下载的源码是Maven工程就别自己改成传统结构反之亦然别在两种结构之间来回折腾。导入完成后修改数据库连接配置文件。这个文件在SSM项目中一般是jdbc.properties位于resources目录下。重点检查数据库地址、账号、密码以及驱动类。jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的数据库密码如果你在jdbc.url里少了useSSLfalse和serverTimezoneAsia/ShanghaiMySQL 8.0可能直接报时区错误或SSL连接警告这都是历届学弟学妹踩过很多次的坑了直接照抄上面配置就行。另外提醒一句MySQL 8.0无论如何都不用com.mysql.jdbc.Driver而是用com.mysql.cj.jdbc.Driver这一点和5.7不一样改错必报ClassNotFoundException。4.3 启动调试全过程与常见启动错误配置完数据库后在IDEA里配置Tomcat。打开Run - Edit Configurations点击加号选择Tomcat Server Local在Deployment选项卡里添加exploded的Artifact比如library_management_war_exploded设置Application context为/library或者干脆/然后Startup/Connection里的端口一般默认8080如果端口被占用就改成一个不常用的端口比如8081。启动Tomcat时控制台是信息最密集的地方。最常见的错误从日志里就能看出端倪。如果控制台出现org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)说明MyBatis的Mapper接口和Mapper XML文件没绑定上。原因基本是两种一是MapperScannerConfigurer扫描的包路径和Mapper接口所在的包路径不一致二是mybatis-config.xml里mapper resource配置的路径写错了或者根本就没配置。整理一下思路这类报错的本质是Spring在容器里注册了Mapper接口的代理Bean但它找不到对应的SQL语句。你还要注意resources目录下Mapper XML文件的目录层级是否和Java的包目录一一对应如果你的Mapper接口在com.example.dao包那么XML文件的路径最好也在resources/com/example/dao下。如果数据库连接相关报错比如Cannot create PoolableConnectionFactory那就是JDBC连接有问题多半是用户名密码错误、IP端口不通、驱动类不对按这个顺序排查。如果出现java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet那就说明SpringMVC的jar包没有打进Artifact的WEB-INF/lib里。解决办法是File - Project Structure - Artifacts里把Available Elements中的Library jars添加进去或者在Output Layout右键Put into Output Root。这种问题常在从非Maven项目导入的时候出现。我把启动阶段最典型的错误和定位思路整理成了一张速查表贴在正文里方便你对照排查。错误现象根本原因排查方向Invalid bound statement (not found)Mapper XML未加载检查mybatis-config.xml的mapper配置、扫描路径、XML路径Cannot create PoolableConnectionFactory数据库连接失败检查jdbc.properties的地址和账号密码ClassNotFoundException: DispatcherServlet依赖未打包进Artifact重新配置Artifact导入Library jarsNo mapping found for HTTP request请求路径没匹配Controller检查RequestMapping路径与前端请求路径是否一致500 空白页Controller方法异常看IDEA控制台完整堆栈常见的NullPointerException和SQLExceptionPort 8080 was already in use端口被占用改Tomcat端口或结束占用进程4.4 从源码到答辩演示一份数据准备清单项目启动成功之后不要急着开始截图给老师看。我真心建议你把演示数据准备好这决定了答辩时演示环节是否顺畅。管理员账号一个用户名admin密码123456普通用户两个用户名student1/student2。座位数据要足够典型至少两个阅览室每个阅览室最好有8到15个座位座位编号要有规律比如101-1、101-2这种命名方式让评委在界面上很容易看懂哪个房间哪个座位。预约记录准备几条不同状态的一条已预约状态0、一条已签到使用中状态1、一条已完成状态2、一条已取消状态3最好还有一条违约记录。这样你给评委演示的时候每一种状态都能点开给老师看不用现场临时操作去等时间流逝让记录变状态。这是经验之谈很多人演示时发现所有预约记录全是已预约一时改不了状态场面会有点尴尬。另外一个很加分的小操作是准备好两个浏览器窗口或者两个不同浏览器一个用管理员登录一个用普通用户登录。这样现场演示时你能表演学生端预约一个座位 → 管理员端立刻看到这条预约记录的实时联动效果比单窗口切换账号节省时间也更能说明系统是真实两端联动的。5. 常见问题排查与避坑笔记5.1 JSP页面取不到数据三层结构传参细节运行中非常常见的一个问题页面能打开但表格是空的或者显示${list}没有被解析。这个通常是EL表达式或者JSTL的问题。SSM JSP的联调里Controller返回的视图名对应JSP页面ModelAndView里塞的数据由JSP用EL表达式取出。要注意几个细节第一JSP页面开头必须有% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %否则c:forEach不可用第二isELIgnored属性如果被设置成trueEL表达式直接不解析输出原样字符串第三从Controller塞到Model里的key要和JSP里写的一致比如Controller里放了model.addAttribute(reservationList, list)那么JSP里必须用${reservationList}拼错字母是典型的低级错误。还有一个排查技巧先用${reservationList}的fn:length或者直接在页面上输出${reservationList}看有没有显示对象引用如果对象存在再检查是循环标签的问题还是字段名的问题。如果字段名和实体类的getter对不上比如数据库字段是res_date实体类属性是resDateMyBatis开启驼峰映射配置之后可以自动转换如果没有开启或者配置写错那查出来的数据会有一堆null。在mybatis-config.xml中开启驼峰映射的配置是这样settings setting namemapUnderscoreToCamelCase valuetrue/ /settings5.2 并发预约冲突和超时未签到两个核心难点怎么应付并发冲突和自动违约这两个点在我看来是这个系统里最有技术含量、也最适合在答辩时讲深讲透的内容。先说并发冲突。我先提一个方案应用层同步锁不可靠。如果你在Controller或Service中简单用synchronized关键字锁住方法确实可以保证单机环境下的串行化但在真实的分布式部署中一个服务有多个实例同步锁毫无作用。不过在答辩场景下你的系统部署在单台Tomcat用synchronized其实也能工作但面试或答辩老师通常更期待听到数据库层的方案。我在3.2节已经给出了用SELECT ... FOR UPDATE做行锁的SQL思路你还可以被追问时补充一个乐观锁方案在座位表或预约记录表加一个version字段更新时校验版本号。这两种方案的取舍可以简单概括写操作冲突频率高用悲观锁冲突频率低用乐观锁。预约系统午间高峰期冲突不低所以我用悲观锁。再说超时未签到自动违约。最草率的实现方式是在用户查询座位或管理员查记录时顺带判断有没有超时的记录然后改状态。这种做法的问题在于状态更新非常被动如果一直没人查询过期记录永远在那里。正规做法是使用定时任务。SSM项目里在Spring配置文件开启task:annotation-driven/然后在Service类上写一个带Scheduled(cron 0 */5 * * * ?)的方法每5分钟扫描一次预约记录表并更新超时的记录。Component public class ReservationTask { Autowired private ReservationMapper reservationMapper; Scheduled(cron 0 */5 * * * ?) public void autoCancelExpiredReservations() { Date now new Date(); ListReservation expiredList reservationMapper.selectExpiredReservations(now); for (Reservation r : expiredList) { reservationMapper.updateStatus(r.getId(), 4); // 标记为违约 userMapper.increaseViolationCount(r.getUserId()); seatMapper.updateStatus(r.getSeatId(), 0); // 座位释放 } } }注意这个定时任务里涉及三个表的更新应该加上事务控制。在autoCancelExpiredReservations方法上标注Transactional(rollbackFor Exception.class)保证同一个用户的多条记录处理不会出现预约状态改了但违约次数没加这种数据错乱。另外定时任务的执行时间间隔不需要太短5分钟或10分钟一次足够每分钟一次对数据库压力偏大这也算一个可以跟老师讲的工程权衡。5.3 关于源码的合理使用怎么把别人的项目变成自己的最后聊聊一个比较现实的问题。标题里写着附源码很多同学下载了源码却不知道怎么把别人的东西合理转化成自己能讲清楚的东西。我见过太多学生在答辩时拿着网上下的项目老师问他某个页面是怎么实现数据展示的他支支吾吾答不上来最后分数惨淡。我的建议分三步走。第一步是改造而非照抄。不要拿源码直接启动然后截图这只是运行了一个项目完全没有变成你的能力。你要把核心代码自己重新写一遍改表结构、改类名、改页面样式至少做到不看源码自己能把预约流程写通。如果有时间把原来的管理员审核模式改成预约即生效自动签到模式功能变了逻辑就很难说是抄的了。第二步是准备技术问答清单。把代码中每块核心功能自己给自己讲一遍这段代码用到什么设计模式、为什么这里返回JSON字符串而不是转发页面、这个SQL为什么用LEFT JOIN不用INNER JOIN想清楚这些答辩提问环节才有底气。第三步是做项目笔记。把你踩过的每一个坑记录下来包括报错信息、排查思路、最终解决方案。这些笔记不但能帮助你在答辩前复习更能成为你博客论文或者项目文档的素材。就拿图书馆预约管理系统这个项目来说网上的源码质量参差不齐有些版本甚至带着病毒或后门代码这也是一些同学导入项目后出现奇怪弹窗或文件被加密勒索的原因。如果你在未知渠道下载了源码强烈建议先杀毒再检查pom.xml或lib目录下有没有奇怪的第三方依赖。我遇到过有同学下载的集成包里被塞了挖矿程序CPU占用率一直99%排查了半天才发现是源码包里面的问题。踏踏实实用官方Maven仓库依赖不盲目引入不明来源的jar包这个习惯从毕设阶段就应该养成。5.4 答辩前的功能演示脚本和时间控制答辩演示环节比多数同学想象的重要得多认真准备一套演示脚本效果比临场随机演示好太多了。我的建议是整体演示控制在8到10分钟不要贪多。开场先花一分钟展示系统整体界面介绍角色和核心功能然后两分钟演示学生端的完整预约流程——查座位、选时段、提交预约、查看预约记录再用两分钟切换到管理员端展示用户管理、座位管理和查看预约记录剩下三分钟展示统计图表和公告发布这两个亮点功能最后留一分钟给老师提问。演示时的设备和数据准备也有讲究。最好使用本机环境演示IDEA里直接启动不要用云服务器或临时部署到远程因为现场网络不稳定会出各种状况。数据库里把演示数据处理好不要有敏感或奇怪的测试数据。提前把Tomcat启动好IDEA保持打开状态模拟器或浏览器都准备好减少现场切换时间。另外建议准备一份项目的演示视频备份在U盘里万一现场系统挂了可以放视频应急。这些都是实际答辩现场最容易忽略的细节提前花20分钟准备就能避免当场翻车。最后我个人的体会是无论是毕业设计还是以后工作里的项目遇到的每一个报错都是最好的学习机会。报错信息就是系统在告诉你它哪里不舒服你按照日志一层层排查下去技术能力就是这样一点点堆起来的。图书馆预约系统看起来功能不多但只要你把需求分析、表结构设计、事务控制、定时任务、权限拦截这一整套完整走下来你对Java Web开发的理解和独立解决问题的能力都会上一个台阶。祝每位同学都能顺利通过答辩。
返回列表