ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot毕设必看:自习室预约系统从并发处理到部署全解析

Spring Boot毕设必看:自习室预约系统从并发处理到部署全解析 前阵子有个学弟拿着毕业设计选题清单来找我第一屏就是“基于Spring Boot的自习室预约系统设计与实现”。他问我这题是不是太大众了做出来会不会没有亮点我说你先别急着追求“看起来很牛”先想想一个问题同样一个座位两个同学在同一秒点预约你怎么保证只放给其中一个人。他愣了几秒然后乖乖回去补并发基础去了。自习室预约系统是Spring Boot方向里非常典型的一类业务系统它没有复杂的算法没有花哨的人工智能但覆盖了完整的项目开发链路用户管理、信息发布、座位资源管理、预约业务、状态流转、定时任务、权限拦截、前后端联调、部署上线。这套东西做完你对Spring Boot的理解绝不止停留在“会写几个增删改查”的程度。这篇文章我打算把这个题目从选题到设计、从编码到部署完整拆一遍把中间真正值钱的细节和容易踩的坑都摊开说适合正在选毕设题目、或者已经选了类似题目但还不知道从哪下手的同学参考。1. 选题定位为什么自习室预约系统是毕业设计的“高性价比”题目1.1 核心需求拆解这题的考点其实很明确很多同学看到一个管理系统类题目第一反应就是“不就是CRUD吗”。我不否认大部分页面确实是增删改查但如果只是这么理解你的系统大概率只能做成一个带页面的Excel。我们先把需求拆开。一个完整的自习室预约系统站在不同角色角度考点完全不一样。用户端注册登录、浏览自习室、查看座位状态、发起预约、取消预约、签到签退、查看个人预约记录和个人违约记录。管理端自习室信息增删改查、座位信息维护、预约订单管理、用户管理、违约记录管理、统计数据展示。这么一列你马上能发现几个看似普通、实际很考验逻辑的地方座位状态怎么实时更新、预约时间和自习室开放时间怎么校验、用户预约次数和违约次数怎么限制、多个用户同时抢同一个座位时系统会不会崩。这些点才是这个题目的真正价值所在。毕业设计答辩时老师问得最多的通常不是“你这个页面好看吗”而是“你这里的高并发冲突是怎么处理的”“你的状态是怎么维护的”“如果用户超时没来怎么办”。这些问题恰恰是普通CRUD项目撑不住、而自习室预约系统天生就要面对的问题。1.2 同类题目对比它和图书管理系统、商品管理系统差在哪每年毕设里还有两个高频题目基于Spring Boot的图书借阅管理系统、基于Spring Boot的商品管理系统。这三个题目表面上看很像实际侧重点完全不同。图书借阅管理系统的核心是“借、还、续借、逾期”它的业务难点在还书日期计算和逾期罚款本质上是一套以“状态更新”和“时间计算”为主的流程。商品管理系统的核心是“分类、库存、购物车、订单”如果做的是真实商城难点在购物车合并、库存扣减和订单状态机如果只是后台管理页面那就纯粹是CRUD了。自习室预约系统不一样它的核心是“稀缺资源在时间维度上的分配”同一个座位只有空闲状态才能被预约预约成功后立刻变成占用状态这中间涉及并发控制、超时释放、违约判定业务逻辑非常自洽也更容易讲清楚。所以我给学生选型的建议是如果你想把毕设重心放在“逻辑完整性和并发处理”上自习室预约是很合适的选择如果你想突出“文件上传、权限树、复杂报表”图书和商品类题目可能更顺手。别盲目跟风想清楚你自己想在答辩时展示什么能力。1.3 技术栈选型为什么是 Spring Boot Vue现在毕设主流组合基本是Spring Boot Vue前后端分离少数学校还在用JSP或者Thymeleaf模板渲染。我的看法很直接只要老师不强制尽量用前后端分离方案。从开发效率上看Spring Boot内置了Tomcat不用再打war包丢到外部容器里一个java -jar就能启动这对环境折腾能力普遍不强的学生来说极其友好。从项目结构上看Spring Boot的自动装配和Starter机制大大简化了配置工作你引入一个spring-boot-starter-webWeb环境就齐了引入一个mybatis-plus-boot-starter数据库操作基础设施也齐了不用像SSM时代那样手动写一堆XML配置。前端用Vue是因为它的组件化开发思路贴近实际工作而且Vue Element UI或者Vue 3 Element Plus做后台管理界面非常快一个表格页加一个弹窗表单半天能搭出三四个页面。下表是我针对这个题目整理的一套技术选型基本可以无脑照抄层次技术选型说明后端框架Spring Boot 2.7.x稳定版本兼容JDK 8避免上3.x后遇到的JDK版本问题持久层MyBatis-Plus单表CRUD不用写SQL复杂查询用lambda条件构造器数据库MySQL 8.x学生机最常见资料多、问题好查权限控制JWT 拦截器登录后返回token前端请求头携带拦截器校验前端Vue 2 Element UI / Vue 3 Element Plus按你熟悉的来页面差别不大构建工具Maven学校机器基本都有比Gradle更通用缓存Redis可选用来存座位状态和预约锁毕设不做也不影响主体功能这里特别说一下版本问题。热词里经常搜“springboot版本太高”这个我是有切身体会的。Spring Boot 3.x发布后一些同学图省事直接选了最新版结果JDK 8跑不起来因为Spring Boot 3强制要求JDK 17或更高还有javax.servlet包迁移成了jakarta.servlet网上很多老代码直接不能用。如果你平时用的电脑就是JDK 8别犹豫选Spring Boot 2.7.x稳定且资料全足够完成毕设。2. 工程搭建与项目结构一上来就把地基打牢2.1 标准目录结构长什么样做毕设最容易犯的毛病就是所有类都扔在一个包里Controller、Service、Mapper混着放。你自己写的时候觉得没什么等过两天回来看找一个小问题得翻半天文件。这个问题在答辩现场很容易被老师挑刺。一个清晰规范的Spring Boot项目结构通常长这样src/main/java ├── com.example.studyroom │ ├── StudyRoomApplication.java // 启动类 │ ├── common // 通用模块 │ │ ├── Result.java // 统一返回结果 │ │ ├── ResultCode.java // 状态码枚举 │ │ ├── GlobalExceptionHandler.java // 全局异常处理 │ │ └── JwtUtil.java // JWT工具类 │ ├── config // 配置类 │ │ ├── WebMvcConfig.java // 放拦截器、跨域配置 │ │ └── MybatisPlusConfig.java // 分页插件 │ ├── controller // 控制层 │ │ ├── AuthController.java │ │ ├── StudyRoomController.java │ │ ├── SeatController.java │ │ └── AppointmentController.java │ ├── service // 业务层 │ │ ├── UserService.java │ │ ├── AppointmentService.java │ │ └── impl │ ├── mapper // 数据访问层 │ ├── entity // 数据库实体 │ └── dto // 传输对象比如登录请求参数分层方式就是经典的三层架构Controller只接收参数、调用Service、返回ResultService写业务逻辑Mapper操作数据库。实体类放entity前端传参如果和实体不是一一对应就建个dto接收不要拿Map到处接参数。之所以要这么拆是因为它符合“关注点分离”Controller层是门卫只做参数校验和结果转发Service层是业务大脑负责预约状态的判断、事务控制Mapper层是搬运工只负责和数据库交互。写代码时你会慢慢感受到分层清晰后排查问题的速度会快非常多。2.2 配置文件从开发到部署只改一份application.ymlSpring Boot的配置集中在application.yml里这个文件管理着你项目的全局环境。下面是一份常见的核心配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/study_room?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: isDeleted logic-delete-value: 1 logic-not-delete-value: 0 logging: level: com.example.studyroom.mapper: debug这里有几个点很容易踩坑。第一MySQL连接串必须带serverTimezoneAsia/Shanghai不然你本机时间和数据库时间对不上预约超时判断全是错的。第二Spring Boot 2.7.x对应的MySQL驱动类名是com.mysql.cj.jdbc.Driver别再用老的com.mysql.jdbc.Driver。第三MyBatis-Plus开启map-underscore-to-camel-case后数据库字段user_name自动映射到实体属性userName这个配置极大减少无谓的TableField注解。补充一个实用习惯把配置环境拆成开发和生产。你可以在pom.xml里配置profileapplication-dev.yml放本机数据库和调试日志application-prod.yml放服务器数据库和精简日志启动时用--spring.profiles.activedev指定环境。这样你本地调久了也不会改坏服务器配置。这个习惯后期部署时会给你省很大力气。2.3 顺带理解Spring Boot的自动装配热词里有“springboot自动装配原理”很多同学背过原理但不太清楚它和项目的关系。我尽量用最简单的方式讲明白。你的启动类上有一个SpringBootApplication注解它实际上是三个注解的组合Configuration标注这是个配置类ComponentScan负责扫描你项目里的Controller、Service等组件最关键的是EnableAutoConfiguration它会在启动时去读所有依赖jar包里的META-INF/spring.factories文件把里面声明的自动配置类都加载进来。自动配置类上通常有ConditionalOnClass、ConditionalOnMissingBean这类条件注解意思就是“你引入了某个依赖、容器里还没有相关Bean我才帮你配一份默认的”。比如Spring Boot里有一堆RedisTemplate自动配置如果你只是引入了starter没有关闭它容器启动后自动就多了template。理解了这一层你后面如果想“自定义自动配置”比如把你这个预约系统里通用的登录校验逻辑抽成一个可复用的starter原理也就是同样的套路写一个配置类加条件注解注册到spring.factories里。做毕设时不需要真干这件事但答辩时如果老师深挖你能说出这个机制印象分会明显不一样。3. 核心业务设计预约系统的数据库与状态机3.1 数据表设计五张核心表字段怎么定先看整体表结构。一个功能完整但不过度设计的自习室预约系统至少有六张核心表用户表、自习室表、座位表、预约记录表、违约记录表、公告表可选。我把关键字段列出来实际写SQL时还有一些冗余字段可以自行调整。用户表id、username、password、nickname、phone、role0学生/1管理员、status0正常/1禁用、create_time。自习室表id、name、location、open_time例如08:00、close_time例如22:00、description。座位表id、study_room_id、seat_number、type普通/靠窗/静音、status0空闲/1已预约/2占用、is_deleted。预约记录表id、user_id、seat_id、study_room_id、appointment_date、start_time、end_time、status0待签到/1已签到/2已取消/3超时失效/4已签退、create_time、update_time。违约记录表id、user_id、appointment_id、reason、create_time。设计的时候记住一句话所有表的主键用自增id或雪花id都行但业务字段里凡是需要频繁查询的一定要建索引比如预约记录表的user_id、seat_id、appointment_date组合索引。不然一旦表里数据到了一万条以上列表查询会明显变慢答辩演示时会很尴尬。预约表里冗余study_room_id和seat_id这两个字段是为了前端列表展示时少做两次关联查询。很多同学喜欢把一切都拆得特别干净查询时join来join去数据量一上来就慢。适当的冗余字段是合理的空间换时间策略。3.2 预约状态流转待签到、已签到、取消、失效预约记录表里的status字段是整个系统里最值得花时间设计的点。不要只把它当做一个普通int而要把它理解成一条“状态机”。用户发起预约后记录状态是0待签到。用户在预约开台前后一段时间内到达自习室并点击签到状态变为1已签到同时座位表里的对应座位status变为2占用。用户在使用结束后点击签退或者系统在用户达到预约结束时间后自动处理状态变为4已签退。用户如果想取消只能在预约开始前取消取消后状态变为2已取消。如果用户预约了但一直没有签到过了预约开始时间一定时长后系统通过定时任务把记录标记为3超时失效同时释放座位并给用户写入一条违约记录。这个流转过程非常清晰很适合画一张表格放进毕业论文的“系统设计”章节当前状态触发事件下一状态座位状态变化待签到手动签到已签到空闲→占用待签到超时未签到超时失效空闲→空闲待签到取消预约已取消空闲→空闲已签到签退/自动结束已签退占用→空闲把状态机设计清楚之后你写的代码就不容易出现“状态值满天飞、一堆if else到底哪个先哪个后”这种混乱局面。系统里凡是涉及状态变更的地方都不要允许跨级流转比如你不应该直接把待签到的记录改成已签退必须走“已签到→已签退”这条路径。这个约束在Service层加判断即可。3.3 并发抢座座位为什么不会被重复预约很多同学在看到并发抢座之前会把预约逻辑写成这样先查座位状态如果空闲就更新座位状态并插入预约记录。这听起来没错但仔细想想就发现问题了。两个请求同时查到了座位是空闲的然后都执行后续的更新操作结果两个人都预约成功可座位只有一个。这就是典型的“并发超卖”。正确做法其实很朴素把“判断座位空闲并锁定”这一步做成一个原子操作。用一条update语句带上条件status0让数据库自己保证同一时间只有一个事务能把这条记录更新成一个值。伪代码如下Transactional public boolean reserveSeat(Long seatId, Long userId, AppointParam param) { // 1. 原子更新座位状态只有原来status0空闲的座位才能被更新为1已预约 int rows seatMapper.updateStatusById(seatId, 1); if (rows 0) { throw new BusinessException(该座位已被预约请选择其他座位); } // 2. 插入预约记录 Appointment appointment new Appointment(); // ... 设置字段 appointmentMapper.insert(appointment); // 3. 事务提交座位状态和预约记录要么一起成功要么一起失败 return true; }这段话请你认真读三遍。核心就一句话先抢锁再写记录抢不到直接失败。这里的“锁”就是座位表里那一条记录的status字段通过update语句的条件更新完成了数据库层面的行级锁。然后整套逻辑放在一个事务方法里使用Transactional保证seat状态更新和预约记录插入操作要么一起生效要么一起回滚。如果你选用了Redis做缓存还可以在此基础上使用Redis的setnx命令作为分布式锁来保护预约操作。但前提是本地MySQL的事务方案已经稳定跑通再把Redis加进来做性能优化别一上来就整复杂架构。4. 核心功能实现登录、预约、查询的思路与代码4.1 登录鉴权用拦截器处理未登录请求做毕设时很多同学对权限控制的理解是“登录了才有按钮显示”但实际上后端必须做真正的鉴权没带有效凭证的人不该调用接口。这里我推荐用最轻量的方式登录时使用JWT生成token返回给前端前端每次请求在请求头里带Authorization字段后端通过拦截器校验。拦截器代码大致是这样的逻辑public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行登录接口 String uri request.getRequestURI(); if (uri.contains(/auth/login) || uri.contains(/auth/register)) { return true; } String token request.getHeader(Authorization); if (token null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } return true; } }然后你在WebMvcConfig里注册这个拦截器并且排除掉static目录下的静态资源和Swagger相关的路径。注册方式我就不贴了这属于Spring Boot基本操作。为什么要用拦截器而不是在每个Controller里手动判断因为如果你手动判断新增一个接口就容易忘而且代码重复度高。拦截器统一处理整个项目的安全边界就非常清晰。答辩时老师问“怎么控制用户只能操作自己的数据”你就可以讲登录成功后在Service层从token解析出用户id所有涉及用户数据的查询都带上这个id从接口层面保证了你只能查自己的预约记录。4.2 预约接口先锁座位再写预约记录预约接口是系统里最核心的接口没有之一。我把它完整拆开讲。Controller层只需要做参数接收和调用Service不要放业务逻辑RestController RequestMapping(/api/appointment) public class AppointmentController { Resource private AppointmentService appointmentService; PostMapping(/reserve) public Result reserve(RequestBody ReserveDTO dto) { Long userId JwtUtil.getUserId(); appointmentService.reserveSeat(userId, dto); return Result.success(); } }Service层就是前面提到的核心逻辑校验时间合法性、校验预约规则、原子更新座位、插入预约记录。这里我补一个容易被忽略的细节预约时一定要校验你请求里传的时间段是否合法。比如自习室开放时间是08:00到22:00用户传了一笔20:00到23:00的预约系统必须拦截。再比如用户已经有一个待签到的预约了还没使用完就不允许再预约其他座位这个校验要用一条count查询搞定。取消预约的逻辑也要注意只有status0待签到的记录才能取消已签到的不能取消已取消的不能重复取消。说白了还是状态机约束。另外取消后要把座位状态恢复成空闲事务同样要包裹两层操作。4.3 动态查询MyBatis-Plus封装的几个实用姿势既然选了MyBatis-Plus就一定要把它用好不然还不如手写MyBatis。查询预约记录列表时常见筛选条件有用户id、日期、状态、自习室id。用LambdaQueryWrapper可以这样写Override public PageAppointmentDTO getAppointmentList(Long userId, Integer status, String date, int page, int size) { LambdaQueryWrapperAppointment wrapper new LambdaQueryWrapper(); if (userId ! null) { wrapper.eq(Appointment::getUserId, userId); } if (status ! null) { wrapper.eq(Appointment::getStatus, status); } if (StringUtils.hasText(date)) { wrapper.eq(Appointment::getAppointmentDate, date); } wrapper.orderByDesc(Appointment::getCreateTime); PageAppointment p new Page(page, size); appointmentMapper.selectPage(p, wrapper); // 然后循环把座位号、自习室名称等冗余字段填充进DTO返回 }这里不需要手动拼SQL条件构造器会自动拼接安全的SQL还能避免“用户输入or 11”之类的注入风险。分页则通过MyBatis-Plus的分页插件实现在配置类里加一个MybatisPlusInterceptor并且注册PaginationInnerInterceptor即可。实战中还有一个高频操作批量查询。比如根据预约记录查出所有关联的座位和自习室千万不要写for循环里selectById而是收集id列表后批量查询一次。能用一条SQL干完的事别做多次网络往返这点在答辩优化提问环节非常加分。4.4 联调细节跨域和日期格式前后端分离联调阶段最常见的两个问题跨域和非法的日期格式。如果前端和后端分别运行在不同的端口比如前端在5173、后端在8080浏览器就会发生跨域问题。解决办法一般是在后端配置CORS也可以在WebMvcConfig里加一个CorsRegistry。记住配置跨域时要明确allowedOriginPatterns别直接放开“*”加allowCredentials组合那样一些浏览器会拒绝响应。日期格式问题更经典。后端返回的LocalDateTime默认可能是一长串带T的格式前端展示成了“2025-05-01T10:00:00”特别丑。正确做法就是在application.yml里配置Jackson的日期格式就是2.2节里date-format和time-zone那段。如果接口里单独用了LocalDate、LocalTime还要额外引入jackson-datatype-jsr310依赖并配置JavaTimeModule这个坑很多人只有在联调时才遇到。5. 定时任务与自动化预约超时、违约记录的落地方案5.1 哪些功能适合用定时任务自习室预约系统里有两类事件天然适合用定时任务来处理。第一类是预约超时失效。比如你规定“预约开始时间超过15分钟仍未签到预约自动取消”。这个动作不能只在某个接口触发时顺带检查因为你永远不知道用户会不会来系统必须有一个后台的“扫地机器人”定时扫描。第二类是使用结束后的座位释放。比如用户预约14:00到16:0016:00签退但如果用户一直不操作不能在17:00时还显示这个座位占用。定时任务可以扫描已签到状态且end_time小于当前时间的记录将其置为已签退并释放座位。在Spring Boot里实现定时任务很简单在启动类加EnableScheduling然后在业务方法上加Scheduled注解。核心代码大概长这样Component public class AppointmentTask { Resource private AppointmentService appointmentService; // 每5分钟执行一次处理超时未签到的预约 Scheduled(fixedDelay 5 * 60 * 1000) public void handleTimeoutAppointments() { appointmentService.markTimeoutAppointments(); } }这里建议你用fixedDelay控制“上一次执行完成后再隔5分钟执行”而不是用fixedRate只看开始时间间隔。原因很简单如果某次执行因为数据量大超过5分钟fixedRate会造成任务堆积而fixedDelay不会它保证同一时刻只有一次任务在跑。5.2 定时任务线程池与执行细节说到定时任务必须提一个隐藏的深坑Scheduled默认是单线程串行执行的。如果系统里有多个定时方法某个方法执行时间过长其他任务会被阻塞在后面排队。等你真正部署上线后会看到日志里某个任务隔了很久才执行一次就是被其他任务卡住了。解决办法是配置一个TaskScheduler线程池。写一个配置类设置线程池的核心线程数推荐4左右。代码如下Configuration public class SchedulingConfig { Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler new ThreadPoolTaskScheduler(); scheduler.setPoolSize(4); scheduler.setThreadNamePrefix(task-scheduler-); return scheduler; } }另外定时任务里操作数据库时要特别小心事务边界。比如每次扫描有几百条超时记录需要批量更新建议一次性查出id列表批量执行update而不是逐条循环处理避免单线程跑太久。执行完任务后最好打一条日志记录本次处理了多少条记录这样排查问题时能看到任务有没有正常跑起来。还有一点和部署相关如果后期你把应用部署在服务器上服务器时间和数据库时间都要统一指向Asia/Shanghai。很多同学本地好好的一部署就出现预约超时时间差8小时的问题排查半天最后发现是服务器时区是UTC。这个坑我在第6章还会再提一次因为它出现的频率实在太高了。6. 部署与打包前端Vue如何塞进Spring Boot6.1 后端jar打包和启动后端打包很简单Maven项目直接用package命令就行。但打包前有几个细节必须检查数据库连接信息是否改成了测试库或服务器库日志级别是不是调到了info还有Resource目录下有没有误放本地测试文件。生产环境启动命令一般是nohup java -jar studyroom-system.jar --server.port8080 --spring.profiles.activeprod app.log 21 这里加了nohup和意思是后台运行并记录日志。注意把app.log路径放在方便查看的位置排查问题时直接tail -f app.log就能看到实时输出。还有一个细节如果你的服务器内存很小可以限制JVM堆内存java -Xms256m -Xmx512m -jar studyroom-system.jar毕设项目用不到太大堆内存512MB绰绰有余。6.2 Vue打包放进Spring Boot的两种方式前后端分离项目中“部署”这件事通常有两种玩法。第一种是主流做法前端打包成静态文件交给Nginx服务器托管后端Spring Boot单独跑一个端口两者通过反向代理对接。第二种比较适合毕设的省事方案就是直接把前端打包产物放到Spring Boot项目的resources/static目录下让Spring Boot同时托管后端接口和前端页面一个jar包搞定所有。我详细讲讲第二种的实操路径。先在Vue项目里调整两个地方Vue Router使用createWebHashHistory或者history模式注意后端回退配置vite.config.js或vue.config.js里设置publicPath/base为./或空字符串确保构建后的资源引用是相对路径。然后在项目根目录执行npm run build生成一个dist文件夹里面是index.html和css、js等静态资源。接下来把这个dist目录里的所有内容复制到Spring Boot项目的src/main/resources/static目录下重新打包后端。启动Spring Boot后直接访问http://localhost:8080/就能看到前端页面。接口请求路径如果统一是/api开头就不会和静态资源冲突。如果你用的Vue Router是history模式还需要在后端加一个转发规则把非/api路径全部转发到index.html否则刷新页面时会出现404。简单说这种方式牺牲了一点点前后端分离的“面子”换来了部署上的极大省心。如果老师不强调必须用Nginx毕设答辩用这种方式完全OK且很好讲解。6.3 部署时常见的环境坑部署阶段我已经见太多次同学在环境问题上耗费一整天。这里列几个最常见、最典型的问题。第一服务器放行端口。云服务器上除了配置安全组放行8080等自定义端口还要确认防火墙状态否则你本机怎么访问都进不去。第二Java环境版本。服务器上到底装的是JDK 8还是JDK 17要和打包时的环境一致不然会报UnsupportedClassVersionError。第三MySQL 8的url驱动。连接串一定要确认driverClassName为com.mysql.cj.jdbc.Driver且url里带serverTimezone。第四记得初始化数据库表结构。很多人代码包上去了页面访问时提示表不存在才想起来数据库那边只建了连接没建表。另外如果你把Vue包放进了static目录一定要先清掉旧的static内容再复制不然容易出现老文件残留导致页面样式或接口路径错乱。7. 常见问题与排查技巧实录7.1 Spring Boot版本太高引发的连环事故前面讲了选型时的版本问题这里我放一个真实案例。有个同学为了“用最新技术”直接创建了Spring Boot 3.2项目本地JDK也换了17一切正常。等他写完大部分功能后才发现运行环境里一个小依赖是老版本的Shiro库Web组件还是基于javax.servlet写死的而Spring Boot 3已经把整个Servlet API迁移到了jakarta.servlet包下。结果登录功能直接因为没有找到Filter而启动失败。这事的教训是做毕设技术栈稳定比“新”重要。如果你已经不幸用了Spring Boot 3.x遇到javax包导入报错就全局搜索javax.servlet改成jakarta.servlet。如果你在纠结初始版本我再次建议Spring Boot 2.7.x加JDK 8这套组合你查资料时搜索出来的90%内容都能直接用。类似的版本坑还有MyBatis-Plus版本和Spring Boot版本的兼容关系。不要盲目用MyBatis-Plus最新版配老Spring Boot去官方文档看兼容矩阵选一个数据库中能确定的、稳定搭配的版本组合。7.2 MySQL连接串和时区问题“差8小时”几乎成了毕设经典问题。本地测试一切正常部署到服务器后发现用户预约的时间全部比当前时间早8小时或者晚8小时所有定时任务的判断全部错乱。绝大多数情况下就是两个原因MySQL连接串没带serverTimezoneAsia/Shanghai或者服务器操作系统时区不是Asia/Shanghai。处理办法第一步先在你本地的数据库客户端执行select now()看看数据库系统时间是不是正确。第二步检查Spring Boot配置文件的url和jackson.time-zone。第三步检查服务器的date命令输出。这三步排查下来基本能定位98%的时间问题。需要注意的是JVM默认时区不一定和你操作系统时区一致如果还是不对可以在启动jar包时加参数-Duser.timezoneAsia/Shanghai。7.3 接口报错没日志等奇怪问题我在带学生时经常遇到一种情况前端调接口返回500后端控制台却几乎没有错误信息。这通常不是没有日志而是你没有一个统一异常处理机制。当你写了GlobalExceptionHandler处理业务异常和未知异常后一定要在其中打印完整堆栈而不是只返回一个“系统异常”给前端。建议日志记录这样分级业务异常只AppWarn不打印堆栈数据库异常、空指针异常等未知异常AppError打印完整堆栈。这能保证出问题时你在日志里能一眼看到根源同时也不会把业务预期的提示当成错误刷屏。另一个高频问题是“本地接口能通前端项目和后端项目一联调就报401”。这大概率不是代码问题而是前端没有把token放到请求头或者token过期时间太短。检查一下网络请求面板看Authorization字段到底有没有带上。这些问题定位都不难但没经验时容易绕远路。还有一类问题学生最喜欢问“同样的代码他机器能跑我不能跑”。我的第一反应永远是先比较JDK版本、Maven仓库配置和数据库版本。这三样不一致别人的代码在你的机器上跑不起来太正常了。不要急着怀疑代码有bug先保证环境一致再排查。最后说点实际的做完这个系统我个人体会最深的一点是真正有价值的不是学会了某个框架的api而是你把一个业务从模糊的“要有个自习室预约功能”逐步拆成用户、座位、预约记录、状态机、定时任务、异常处理这些具体模块的过程。这个拆分能力才是毕设结束后能带走的东西。如果你时间比较紧我建议开发顺序按这个来先做数据库和登录注册再做管理员端的自习室和座位管理接着做用户的预约、取消和签到最后补定时任务和部署。每一步都能跑通后再做下一步不要一上来就想把所有页面都堆出来。自习室预约系统看着简单真正做一遍后你一定会遇到一两个让你“卡住”半天的问题但那恰恰是你处理问题能力提升最快的时刻。希望这篇文章能让你在卡住时少走几步弯路。
返回列表