☰
SpringBoot+Vue宿舍管理系统全栈项目实战:从数据库设计到部署上线
2026/10/5 8:24:23 网站建设 项目流程

写这个项目的时候,我连续泡在机房两个通宵,把SpringBoot和Vue之间那点交互逻辑彻底捋了个透。宿舍管理系统是Java Web毕设里最经典、也最容易被问住题目的方向之一——学生会查、宿管要管、辅导员要审批,一套系统把"角色权限""数据库设计""前后端分离"这些核心考点全占了。所以这个完整项目源码加上SQL脚本和接口文档,正好适合正在找毕设项目的同学、以及想快速上手SpringBoot+Vue全栈开发的初学者。我把整个项目的设计思路、核心模块、部署流程和一些踩过的坑完整梳理了一遍,从环境搭建到上线联调,每一步都拆开讲清楚。

这个项目不是那种只给个压缩包就完事的资源。源码、SQL脚本、接口文档三件套配齐,说白了就是把你从"看视频觉得会了"到"真正跑起来自己改"之间那段路直接铺好。我尽量把每一个设计决策背后的原因也写出来,这样就算你后面要拆成别的系统,比如图书馆管理系统、实验室预约系统,也能顺着这套思路改。

1. 项目整体设计与技术选型思路

1.1 为什么是SpringBoot加Vue这套组合

宿舍管理系统这种典型的管理信息系统,核心需求就是两个字:增删改查。听起来简单,但加上"不同角色看到不同界面""审批流程要流转""数据要能统计"这些现实约束之后,前端要有交互流程,后端要管事务逻辑,两头都得硬。

我选SpringBoot的理由很简单:它能让我把精力放在业务逻辑上,而不是浪费在配置XML和搭框架上。SpringBoot内置了Tomcat,写好Controller就能直接跑;配合MyBatis-Plus,单表CRUD几乎不用写SQL;Spring Security做登录认证和角色鉴权,官方文档和社区例子都足够多,毕设答辩被问到也答得上。

Vue这边我选了Vue 2。我知道Vue 3已经稳定好几年了,但做毕设有个现实考量:Vue 2的Element UI组件库特别成熟,表格、表单、弹窗、分页这些管理后台常用的组件全都有现成封装,写起来效率非常高。加上网上的教程和二手资料多,遇到问题一搜就能找到解决方案。如果后续想升Vue 3,组件库换成Element Plus,风格基本一脉相承,迁移成本其实不高。

1.2 宿舍管理系统的需求拆解

很多同学拿到毕设题目就直接开写,写到一半发现权限乱了、数据对不上、流程走不通。我拆解需求的时候,习惯先画一张角色和权限的网格图(这里不说用专业工具,拿Excel就能做):

功能模块学生宿管员辅导员/管理员
宿舍查询只看自己宿舍看管辖楼栋全部数据
入住/退宿提交申请审核并分配查看记录
调宿申请提交申请审核处理审批
报修管理提交/确认完成派单/处理查看统计
晚归/归寝记录查看个人登记/管理查看统计
学生信息管理查看个人维护本楼管理系统全部
系统管理(账号/角色)无权限部分权限完全权限
数据统计与导出无权限本楼数据全校数据

这个网格理清楚之后,后端接口怎么设计就一目了然了——每个角色对应一套接口权限,前端路由用动态路由做控制,后端在SpringSecurity的Filter链里做统一校验。防君子也防小人,前端隐藏入口只是体验层面的,真正兜底的还是后端权限校验。

1.3 项目结构规划

项目分为后端(dormitory-backend)和前端(dormitory-web)两个独立目录,这也是大多数前后端分离项目的常规做法。

后端的包结构我按MVC经典三层来划分:

com.dormitory ├── config // 配置类,放WebMvcConfig、SecurityConfig ├── controller // 控制层,接前端请求,不写业务逻辑 ├── service // 业务层,接口加实现类 ├── mapper // MyBatis-Plus的Mapper接口,继承BaseMapper ├── entity // 数据库表对应的实体类 ├── dto // 接收前端参数的对象,避免直接暴露实体类 ├── vo // 返回给前端展示的对象,例如宿舍详情 ├── utils // 工具类,JWT工具、日期处理等 └── common // 通用结果封装、全局异常处理

前端的src目录按Vue项目惯例来:

src ├── api // 按模块拆分的接口请求文件 ├── assets // 静态资源 ├── components // 公共组件,如上传组件、分页组件 ├── router // 路由配置,动态路由在这里维护 ├── store // Vuex,管理登录状态和用户信息 ├── views // 页面组件,按模块放子目录 ├── utils // 封装的axios实例、工具函数 └── App.vue // 根组件

这个结构有一点值得说明:dto和vo分开,而不是共用一套对象。我见过很多项目让前端传过来的参数直接绑定实体类,结果把createTime、status这种后端才应该管的字段也暴露给了前端。严格区分入参和出参,第一个好处是安全,第二个好处是Swagger生成的接口文档更清晰。你可以多传我不管,但我不想接收的字段你传了我也忽略。

2. 数据库设计与SQL脚本的落地要点

2.1 核心数据表的设计思路

SQL脚本是整个项目的基石,表关系设计得好不好,直接决定了后面写代码是"顺水推舟"还是"处处补丁"。宿舍管理系统的核心表我梳理成了这几类:

第一类是基础信息表,包括用户表(sys_user)、楼栋表(dormitory_building)、宿舍表(dormitory_room)、床位表(dormitory_bed)。第二类是业务流转表,包括入住记录表、调宿申请表、退宿记录表、报修单表和归寝记录表。第三类是系统支撑表,包括角色表、菜单表和角色菜单关联表。

说一下sys_user表的设计:

CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `sex` tinyint(1) DEFAULT NULL COMMENT '性别 0未知 1男 2女', `student_no` varchar(20) DEFAULT NULL COMMENT '学号', `phone` varchar(11) DEFAULT NULL COMMENT '手机号', `role` tinyint(1) NOT NULL DEFAULT '3' COMMENT '角色 1管理员 2宿管 3学生', `building_id` bigint(20) DEFAULT NULL COMMENT '宿舍楼ID', `room_id` bigint(20) DEFAULT NULL COMMENT '宿舍ID', `bed_id` bigint(20) DEFAULT NULL COMMENT '床位ID', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态 1正常 0停用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

这里有个细节:密码字段用varchar(100)而不是varchar(32),因为BCrypt加密后的字符串长度是60位,MD5加密才是32位。我见过有人用固定32位字段存BCrypt结果,结果怎么测都登录失败。

宿舍和床位的设计,我用了"宿舍表+床位表"两张表而不是直接在宿舍表里放"已住人数"和"容纳人数"。后来项目做完复盘,这个设计有两个好处:一是每个床位可以单独关联一个学生,二是查"空床位"时直接查bed_id为null的用户即可,不用做复杂的统计。当然,这个方案会带来一个小问题:如果某学生退宿后床位被释放,但室友间换了床位,关联关系怎么处理?这在后面调宿模块里单独解决了。

2.2 SQL脚本里那些容易被忽略的细节

第一个坑:建库语句建议显式指定字符集:

CREATE DATABASE IF NOT EXISTS `dormitory_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

很多人直接在Navicat里点"新建数据库",默认字符集可能是latin1或者utf8,中文存进去没问题,但查出来显示乱码,排查半天其实字符集没对齐。用utf8mb4不是为了花哨,是因为以前的utf8字符集在MySQL里最多3个字节,存不了emoji和个别生僻字,utf8mb4才是完整版。

第二个坑:导入SQL脚本的方式要和MySQL版本匹配。MySQL 5.7和8.0在默认认证插件上不一样(mysql_native_password和caching_sha2_password),如果你用的是8.0的库,但程序连接时用的驱动版本是5.x,就会报"Public Key Retrieval is not allowed"这个错。解决方式要么在JDBC连接串里加allowPublicKeyRetrieval=true,要么给用户指定旧的认证方式。

第三个坑:脚本末尾不要忘了加索引和初始化数据。尤其是菜单表(sys_menu)和角色菜单关联表,如果不初始化,前端动态路由配了也白配——登录之后菜单不显示,很多人第一反应是代码写错了,实际是SQL脚本少了两行INSERT。

SQL脚本文件我用注释分好段落,每个段落能独立执行,方便排错:

-- 1. 创建数据库 -- 2. 创建表结构(按依赖顺序:先建基础表,再建业务表) -- 3. 初始化基础数据(管理员账号、默认角色、菜单权限) -- 4. 测试数据(仅供开发环境使用,上线前记得清理)

初始化管理员账号的密码写的是123456,但存到库里的是BCrypt加密后的值。我在脚本注释里写清楚了明文密码和对应关系,不然你自己导入脚本后拿明文密码去登录肯定失败。这个细节对拿到项目第一次跑通的同学特别重要。

2.3 表关系设计里最精髓的一张表

宿舍分配这块,我用了一张被很多人忽视的关联表——宿舍事务记录表。所有入住、调宿、退宿、维修确认,都在这张表里留痕。整个系统的关联字段如果不建约束,随意删数据,后面报表统计会很难看。做的过程中我保留了关键外键约束并且索引了外键字段:

ALTER TABLE `dormitory_check_record` ADD KEY `idx_building_id` (`building_id`), ADD KEY `idx_room_id` (`room_id`), ADD KEY `idx_student_id` (`student_id`);

索引不是越多越好,但查询频繁的字段值得加。我遇到过慢SQL,就是查宿舍报表时没走索引导致全表扫描,几万条数据就把接口拖到两三秒。加了索引之后直接回到几十毫秒,这一点后面在优化章节还会再提。

3. 后端核心模块设计与实现

3.1 登录认证与JWT令牌机制

Spring Boot做毕设项目的登录,大概有几个流派:靠Session、靠Spring Security加Session、靠JWT。我做这套系统选用的是JWT方案,因为前后端分离项目的常规姿势就是这样:后端签发Token,前端每次请求在请求头里带Authorization: Bearer <token>,后端拦截器校验Token并解析出用户信息。

JWT的核心逻辑并不复杂,关键就三步:

// 1. 登录成功后生成Token String token = Jwts.builder() .setSubject(username) // 用户名 .claim("role", user.getRole()) // 角色 .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) // 24小时过期 .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); // 2. 在Spring Security的过滤器中校验Token String token = request.getHeader("Authorization").replace("Bearer ", ""); Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); // 3. 解析出用户信息后,放入SecurityContext UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(auth);

这里有一个比较关键的经验:SECRET_KEY不要用太短的字符串,至少32位,最好是字母数字混带一点特殊字符,不然生成签名时容易被反解出来。密钥放在配置文件里,别写死在代码里。虽然毕设项目不会真的被攻击,但是从写代码的习惯上养成安全思维是好事。

还需要注意的是Token过期策略,我给前端约定的是:后端返回code=401表示登录已过期,前端axios拦截器收到这个状态码就自动跳转登录页并清除本地缓存。如果不做这个约定,用户Token过期后项目还会继续请求,每次返回都是错误信息,界面却还停留在操作页,体验很差。

3.2 宿舍分配与调宿的业务逻辑

这个模块是整个系统里最容易写乱的部分,因为牵扯到的事务比较多。分配床位时,我要保证三个一致性:学生当前未被分配、宿舍未满员、目标床位是空床。同一时间只能有一个线程把这个床位分出去,否则并发请求会造成超员。

我在代码里用一个简单但可靠的策略:悲观地先在数据库里把床位标记为"已占用",再更新学生关联。核心逻辑是这样:

@Transactional(rollbackFor = Exception.class) public Result assignRoom(AssignRoomDTO dto) { // 1. 检查学生是否已有宿舍 if (userService.hasRoom(dto.getStudentId())) { return Result.error("该学生已分配过宿舍"); } // 2. 用UPDATE判断床位是否空闲(CAS思想,锁住这行记录) int update = bedMapper.updateStatus(dto.getBedId(), 0, 1); // 参数: where status=0 and id=? if (update == 0) { return Result.error("该床位已被占用,请刷新后重试"); } // 3. 关联学生和床位 userService.bindRoom(dto.getStudentId(), dto.getRoomId(), dto.getBedId()); // 4. 写入分配记录 recordService.addRecord(new CheckRecord(...)); return Result.success(); }

第2步里那个UPDATE语句是关键,它利用数据库行锁把"查询并更新"这个非原子操作变成了原子操作。很多初学者在这里犯的错误是先用SELECT查床位是否为空,再用UPDATE去改状态——中间隔了几微秒,但并发场景下依然会出问题。毕设答辩时把这个逻辑讲清楚,通常能让你高出同学一大截。

调宿流程类似,但多了一步"释放旧床位再分配新床位"。我把这两个操作放在一个事务里,要么都成功,要么都失败回滚。注意这里是先分配新床位成功,再释放旧床位,释放失败会回滚,学生不会出现"两头空"的情况。

3.3 报修模块和晚归记录模块的后端实现要点

报修模块看起来简单,但隐藏着一个状态机的设计问题。我记得自己第一次实现的时候,用了一个整数字段status,取值1、2、3分别代表"待处理""处理中""已完成",然后用if-else去判断状态流转。代码写了不少,逻辑还容易漏。

后来我换了一种方式:状态变更直接由Service层来控制,Controller只负责接收请求,不直接去改状态字段。核心就是一张状态流转表,防止非法跳转:

操作原状态目标状态允许的角色
提交报修无待处理学生
接单待处理处理中宿管
完工确认处理中已完成宿管
验收关闭已完成已关闭学生

学生提交报修单时,我还做了个防抖处理:同一宿舍同一时间段内(比如2小时内)只能提交一次同类报修,防止恶意刷单。这个逻辑在Service层实现:

// 查询最近2小时内同一宿舍同一类型是否已有待处理报修单 long count = repairMapper.selectCount(new LambdaQueryWrapper<Repair>() .eq(Repair::getBuildingId, buildingId) .eq(Repair::getRepairType, repairType) .eq(Repair::getStatus, "待处理") .ge(Repair::getCreateTime, DateUtils.addHours(new Date(), -2))); if (count > 0) { return Result.error("该宿舍已有同类待处理报修单,请勿重复提交"); }

晚归记录模块更简单一点,就是一个登记加查询。但提醒一下:学生端和宿管端看到的字段最好有区别。学生端不需要看到登记人是谁,宿管需要看到,这个通过一个VO对象来切换即可,没必要写两套接口。

4. 接口文档与前端Vue的协作方式

4.1 接口文档怎么设计才不至于前后端互相扯皮

这个项目自带一份接口文档,我把文档和源码放在同级的docs目录下,包含接口说明、请求参数、响应示例和错误码。整个项目接口统一遵循RESTful风格,模块前缀用/api区分。

一个标准接口的约定是这样的,比如宿舍查询:

GET /api/room/list?buildingId=1&pageNum=1&pageSize=10&keyword=501

响应统一格式:

{ "code": 200, "message": "success", "data": { "total": 20, "list": [ { "id": 1, "buildingName": "1号楼", "roomNo": "101", "beds": 6, "usedBeds": 5, "status": "已满", "score": 4.5 } ] } }

我在写接口文档时遵循三个原则:

  • 语义化URL。用名词描述资源,比如/api/student/info,不用动词如/api/getStudentInfo。
  • 明确泛化返回结构。所有接口统一返回一个Result<T>对象,包含code、message、data三个字段。不用约定俗成的"直接返回对象,出错抛异常"这套,因为前端处理完全没有统一入口。
  • 标注权限要求。文档里每个接口都标清楚"学生可调用""宿管可调用""管理员可调用",前端做调试切换身份时就不会糊涂。

写文档的工具我推荐直接用Swagger注解挂在接口上,然后导出成Markdown或者HTML页面,省得另维护文档文件后来跟代码脱节。Swagger注解写起来也不麻烦,几个核心注解就把参数说明、返回值说明覆盖了。

4.2 Vue前端项目的脚手架搭建和路由设计

前端项目的搭建可以借助Vue CLI的交互命令,但实际上手写一把也很快。我用Element UI做组件库,用Axios发请求,用Vuex管理用户状态,用Vue Router做路由。

路由权限是动态的。这里有个常见误区:很多同学在路由表里把所有页面全部注册,然后靠v-if="role===1"控制页面显示。这种做法的问题在于,虽然页面上看不到入口,但通过改路由或者直接访问路径还是能进到页面里。真正的动态路由是,在前端登录后根据后端返回的权限菜单列表,动态注册对应路由组件:

// 路由守卫中根据用户角色过滤路由 router.beforeEach((to, from, next) => { if (!store.getters.token) { if (to.path === '/login') { next(); } else { next('/login'); } } else { // 拉取菜单 -> setRoutes动态注册 -> 确保刷新后不白屏 if (store.getters.routes.length === 0) { store.dispatch('generateRoutes').then(routes => { router.addRoutes(routes); next({ ...to, replace: true }); }); } else { next(); } } });

这个思路能保证前端路由和服务端的菜单权限保持同步,刷新页面时也不会因为路由丢失而白屏。我的经验是,路由权限这块是前端代码里最值得花时间调的部分,也是容易在答辩现场被问到的部分。

4.3 前端Axios封装与跨域问题

前端封装Axios是必要的。我封装了一个request.js,统一处理BaseURL、Token注入、错误提示和401跳转:

const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }); // 请求拦截器:注入token service.interceptors.request.use( config => { if (store.getters.token) { config.headers['Authorization'] = 'Bearer ' + store.getters.token; } return config; }, error => Promise.reject(error) ); // 响应拦截器:统一处理code和401 service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { Message.error(res.message || '请求错误'); return Promise.reject(new Error(res.message || '请求错误')); } return res; }, error => { if (error.response.status === 401) { store.dispatch('logout').then(() => { router.push('/login'); }); } Message.error(error.response.data.message || '网络异常'); return Promise.reject(error); } );

跨域这块在开发环境用Vue CLI的代理很方便:

// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } };

生产环境有两个处理路径:一是后端加@CrossOrigin或全局CORS配置;二是把前端打出来的dist目录直接放到SpringBoot的src/main/resources/static下,同一个端口访问,自然没有跨域问题。这个方向热搜词里也提到了"vue打包放进springboot",我在部署章节会重点说明。

5. 部署流程与联调实录

5.1 本地开发环境与配置检查

跑这个项目的环境是:JDK 8(或11也行)、Maven 3.6+、MySQL 5.7+、Node.js 14+。先依次填好这几处配置,再往下走。

后端配置在application.yml里:

server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/dormitory_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这里说一个比较容易让人卡住的地方:serverTimezone参数。MySQL 8.x的驱动对时区敏感,不配这个参数启动时经常报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。我看到评论区不少同学卡在这个错上,其实就是两个解决办法:一个是在JVM参数里加-Duser.timezone=GMT+8,另一个是URL里配上serverTimezone=Asia/Shanghai。

前端环境配置在根目录的.env.development和.env.production两个文件里:

# .env.development VUE_APP_BASE_API=http://localhost:8081/api VUE_APP_MOCK=false

后端的接口前缀和前端配置要对应上,不然请求路径会404。如果后端Controller的RequestMapping是/api/room,前端请求应该写/room/list,Axios的baseURL里已经带了/api。

5.2 从源码到跑通的完整操作步骤

给第一次接触这个项目的人一个完整步骤,照着做,不要跳步:

  1. 先用Navicat或命令行工具执行dormitory_db.sql脚本,导入数据库。导入之后能看到14张表左右,可以在sys_user表里查一下管理员账号是否初始化成功。
  2. 用IDEA打开dormitory-backend目录,等Maven把依赖下载完。依赖下载慢的话,在settings.xml里把镜像源换成阿里云镜像,不然等到天荒地老。
  3. 改配置文件里的数据库用户名和密码。密码千万记得改成你自己的,不然启动到一半报Access denied for user,很多人都忘在启动之前先检查这一项。
  4. 启动SpringBoot应用。看到日志里出现Started Application in xx seconds并且没有异常,后端就起来了。
  5. 用VS Code或WebStorm打开dormitory-web目录,在终端执行npm install。这里提醒一下,安装依赖卡住时不要反复Ctrl+C,先看是不是镜像源问题。
  6. 执行npm run serve,浏览器访问http://localhost:8080,用管理员账号登录。

管理员账号初始值我通常在文档里写明,比如admin / 123456。学生测试账号可以自己注册或者直接用脚本里的测试数据。

如果一切正常,登录后第一眼看到的应该是系统首页,左侧菜单按角色动态渲染。如果左侧菜单空荡荡,先回数据库确认菜单表和角色表的数据在不在。

5.3 将Vue打包放进SpringBoot中的集中部署

有不少同学问过,开发环境的跨域代理解决了,部署到服务器或者给老师演示时怎么办。最省事的是把Vue打包后的dist目录放进SpringBoot的静态资源目录,然后后端同时管接口和页面。

打包命令:

npm run build

打包完成后,dist目录里有index.html和static子目录。我把dist里的文件整体拷贝到SpringBoot项目的src/main/resources/static目录下,然后重新打包后端。这样部署后访问http://服务器IP:8081,直接就能看到前端页面。

但有一个坑必须提醒:前端路由用的history模式,刷新非首页路由时会404。比如我访问/system/user,刷新页面后端返回404,因为后端找不到对应的Controller。在SpringBoot里解决方法是把404错误重定向到index.html页面:

@Controller public class IndexController { @RequestMapping(value = "/**/{path:[^\\.]*}") public String redirectToIndex() { return "forward:/index.html"; } }

这个Controller的目的是让前端路由刷新时不落空。如果你觉得这个方案有点"绕",也可以把前端路由改成hash模式,URL会带上/#/标记,刷新时就不存在404问题。两者各有利弊,我个人的习惯是history模式体验更好,多写一个转发Controller也不算麻烦。

5.4 配置Maven项目构建与常见启动报错

很多同学第一次用Maven构建SpringBoot项目,会遇到依赖下载失败或者JDK版本不匹配。先说JDK版本,项目pom.xml里明确写成:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent>

这个版本能跑在JDK 8上,且不会出现SpringBoot 3.x那种强制要求JDK 17的尴尬。如果你的本机只有JDK 17,建议还是装个JDK 8或者11,不然踩兼容性坑得不偿失。热搜词里提到"springboot版本太高"导致的问题,大概率就是指SpringBoot 3.x的这些兼容成本。项目里如果用到MyBatis-Plus、Shiro这类老牌组件,官方适配SpringBoot 3还需要多一个桥接依赖,毕设没必要冒这个险。

Maven构建时还容易遇到报错Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin。原因多半是编译级别不对,pom里我设置了:

<properties> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>

如果IDEA里设置了项目的SDK是17,但pom要求1.8,会有一个互不匹配的报错。在项目结构里把Project SDK和Modules的Language level统一改成8,基本就能消掉。

Maven项目的构建还有一种顺序误区:先改了pom文件依赖再手动去下载jar包,其实完全没必要。直接在IDEA右侧Maven工具窗口双击clean和install,依赖会自动拉取。构建完成后target目录下会出现dormitory-0.0.1-SNAPSHOT.jar,整个SpringBoot项目就可以用java -jar运行了。

6. 常见问题排查与避坑心得

6.1 前后端联调时最常见的四个报错

我整理了一份排查速查表,整理了我自己做这个项目时踩过以及被别人追着问过的问题:

现象可能原因排查方向
前端请求接口404API前缀不一致检查request.js里的baseURL和后端RequestMapping前缀
前端请求接口200但数据为空跨域被拦截打开浏览器F12看Console有没有CORS报错
登录提示用户名密码正确但进不去Token没放进请求头检查axios拦截器里Header拼写,Authorization别写错
数据库中文乱码连接串字符集不对URL里加characterEncoding=utf8&useUnicode=true
查询报Unknown column xxxSQL脚本和实体类字段名不一致检查MyBatis-Plus的驼峰映射配置,下划线和驼峰要对应
前端页面刷新后404历史和模式问题使用history模式时后端加转发Controller,或者改hash模式
启动报Consider defining a bean of type xxxMapperMapper接口没加扫描注解启动类加@MapperScan("com.dormitory.mapper")
报Field 'id' doesn't have a default value主键没有设置自增检查表的id字段,AUTO_INCREMENT是否设置,插入时是否显式给了id

这些排查有一个共同原则:先在前后端分离的边界上定位问题。请求到了后端没有,看后端日志就没有对应访问记录,问题多半出在路由或代理;请求到了后端但响应异常,看异常堆栈再决定是SQL问题还是业务问题。

6.2 SpringBoot版本和高版本JDK的兼容性取舍

做这个项目时我特意把SpringBoot版本固定在2.7.x,而不是最新版。有一个热搜词是"springboot版本太高",这个我太有感触了。选型时的新版本往往意味着新特性,但也有很多连带问题:

  • SpringBoot 3.x最低要求JDK 17,很多学校机房、老师电脑上还是JDK 8。
  • SpringBoot 3.x里javax.servlet变成了jakarta.servlet,很多老教程里的import javax.servlet直接报错。
  • MyBatis-Plus、Shiro这类生态组件的适配版本起初没跟上,遇到问题在国内社区能搜到的答案也少一些。

做毕设的原则是稳定压倒一切。SpringBoot 2.7.8足够新,修掉了前代版本一堆漏洞,同时能兼容JDK 8/11。如果读者自己手里有别的项目非要切到SpringBoot 3,也要先检查一遍所有第三方依赖是不是有支持SpringBoot 3的版本,再动手改代码。

6.3 SQL脚本导入失败和SQL性能优化经验

导入SQL脚本时如果报错,最常见的两种:一是脚本里的SQL用了MySQL 8.0特有的语法(比如窗口函数),而在5.7上不支持。另一个是导入时某张表已经存在,没有DROP TABLE IF EXISTS语句。我给的脚本里都处理过这两点,但如果你在自己电脑上导入失败,先看具体报错行号,往往就在这两类问题里。

SQL性能优化上,我做这个系统最深刻的体会是:统计接口的SQL才是测试的大头。宿舍入住率报表要跨表查询:sys_user关联dormitory_room,按楼栋分组统计每个宿舍的已住人数。没建索引之前,这个接口响应在2秒左右;建了联合索引后直接掉到几十毫秒。索引不是银弹,但针对高频查询建索引,改善效果立竿见影。

-- 常见慢SQL及其优化思路 -- 原:对楼栋名做了模糊查询并分组,全表扫描 SELECT r.building_id, COUNT(r.id) FROM dormitory_room r WHERE r.building_id LIKE '%1%' GROUP BY r.building_id; -- 调整:先过滤出需要的楼栋ID,再分组统计 SELECT u.building_id, COUNT(*) FROM sys_user u WHERE u.building_id IN (SELECT id FROM dormitory_building WHERE name LIKE '%1号楼%') GROUP BY u.building_id;

这段优化并不高深,实际工作中最常用的手法:尽量在索引字段上做等值匹配,少在索引字段上用函数和模糊前缀匹配,避免回表次数过多。

6.4 上手这个项目源码的正确打开方式

最后贴点实用心得:拿到别人的项目源码先别急着双击运行,更别急着删掉重写。建议按这条路径读一遍代码:

  1. 先读SQL脚本,花半小时把表结构关系和初始化数据理解透。
  2. 再从后端启动类入手,看config包下的配置类,了解哪些拦截器生效、哪些配置项可以调。
  3. 接着顺着一个完整业务流程"登录→查询列表→提交申请→管理员审批"读代码,把Controller、Service、Mapper这条链路串起来。
  4. 最后打开前端项目,对着页面元素找对应API请求,两边对照着看,才算真正"吃透"项目。

我见过不少学员,跟着视频敲代码的时候挺熟练,考试完就忘。这个项目里最值得研究的其实是宿舍分配那一段的并发控制、动态路由的权限设计,以及报修流程的状态机设计,把这三块搞明白,比把整个项目重抄一遍管用得多。

自己做毕设或者面试作业时,建议先把系统跑起来,再按上面路线读一遍核心代码,最后选一个模块试着改改功能。比如把宿舍管理改成实验室预约,你会发现改流程的时候才真正理解为什么表要这样设计、接口要这样拆分。改动过程中遇到报错,对照我前面整理的排查表,百分之八十的问题都能自己定位解决。

对我来说,写这套系统的最大收获不是"跑起来了",而是理解了"为什么会这样"——为什么数据库要设计五张关联表,为什么宿舍分配要用乐观锁,为什么路由权限不能只在前端控制。把这些"为什么"都想通了,毕业设计的答辩也好,以后在工作中真正接手业务系统也好,才算是真正入了Java Web的门。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询