资讯动态

SpringBoot+Vue学生公寓管理系统:从搭建到部署全解析

发布时间:2026/10/16 15:18:29 来源:尧图企业网站定制
做学生公寓管理这个题目在Java方向里算是最经典的课设选题之一了。SpringBoot做后端、Vue写前端、MySQL存数据这套前后端分离的组合这几年在高校课设和毕设里出现的频率极高。最近常有同学问我这类系统怎么搭、跑起来之后怎么改成自己的项目、答辩该讲哪些点干脆把我常用的一套完整实现和搭建过程整理出来。整套系统功能覆盖公寓楼宇管理、宿舍房间分配、入住退宿登记、水电数据录入、维修上报、来访记录等学生公寓里最常见的几块管理场景代码规模适中既能当毕业设计也能拿来当课程设计作业或者纯粹当做学习前后端分离项目的练手材料。我第一次做这种公寓系统是给某高校宿管部门做的内部管理工具。做完的体会是这类系统表面上需求很直白真正上手依然有很多容易忽略的细节。比如宿舍分配时的容量校验、入住退宿的状态流转、楼层统计报表的聚合逻辑每一个点都能单独展开讲不少东西。下面我把整个项目从需求分析、数据库设计、接口开发、前端页面到部署排错全部过一遍代码和参数都是可以直接照着用的。1. 先理清需求公寓管理系统到底要管什么1.1 传统管理方式的痛点和系统价值在没有系统之前很多高校宿舍管理靠的是Excel表格加纸质登记。新生入学季宿管员要拿着名单一栋楼一栋楼地核对空床位退宿时又要翻登记本找入住记录报修单可能直接夹在文件夹里放一周没人处理。这种模式的问题倒不是不能用而是效率太低、数据不一致、没法做统计。公寓管理系统解决的问题就是把这几件事数字化楼栋和房间信息统一维护学生入住退宿走线上流程报修和访客留痕管理人员随时能看到当前空床位数、各楼栋入住率、水电费缴纳情况。对学生来说能在线提交报修、查看宿舍分配结果对管理员来说一张看板就能掌握全校公寓的实时状态。这也正是这个题目在课设和毕设里受青睐的原因——需求来自真实生活不虚不空功能边界清楚又有足够的业务复杂度。1.2 角色划分与核心业务流程角色设计上我推荐做三类系统管理员、宿管员、学生。管理员负责楼栋宿舍的基础数据维护、管理员账号管理、统计报表查看宿管员负责日常的入住登记、退宿办理、水电费录入、报修处理学生端功能相对轻量主要就是查自己住的房间、提交报修、报修进度查询、登记访客申请。业务流程里最核心的是入住和退宿。入住流程学生报到→宿管员选择所在楼栋和房间→确认房间当前床位余量→填写入住信息→生成入住记录同时宿舍楼该房型的已住人数加一。退宿流程学生申请或宿管员直接办理→校验该生当前处于在住状态→生成退宿记录→房间已住人数减一。这两段流程涉及事务操作数据不能有半点马虎。报修流程也好理解学生提交报修单位置、问题描述、图片选填→宿管员在后台看到待处理列表→指派维修人员或上门处理→状态改成已完成。1.3 功能模块清单我习惯在动手写代码前先列清楚模块这也是课设答辩时老师一定会问的东西。建议拆成下面这些系统登录与权限控制三种角色统一登录入口登录后根据角色渲染不同菜单接口层面做权限拦截。楼栋与宿舍管理增删改查、批量导入导出、按楼栋查看房间列表和入住状态。学生住宿管理入住登记、调宿、退宿、住宿历史查询。报修管理学生提交报修宿管员处理和回访支持上传图片。水电费管理按月录入每间宿舍的水电读数系统自动计算用量和费用。访客登记来访人员信息登记、被访人确认、离开时间登记。统计看板总入住率、各楼栋空床统计、近期报修数量趋势。功能不必贪多上面这些已经足够支撑一个完整的毕设主体。如果时间充裕可以再扩展一个消息通知模块比如入住成功后自动推送通知这块作为加分项效果不错。2. 技术选型与项目骨架搭建2.1 为什么是SpringBootVue而不是其他组合很多同学会纠结用不用更轻的框架比如直接用Thymeleaf模板渲染。我想说一点真实体验如果你的目标是学习当前企业里实际在用的开发模式前后端分离是绕不开的SpringBootVue就是国内中小型项目里最主流的组合之一。Spring Boot负责把后端接口快速落地内嵌Tomcat省去部署麻烦Vue负责SPA单页应用组件化开发、路由控制、状态管理都齐全。Java生态稳定资料多遇到问题基本都能搜到解决方案这对课设和毕设阶段的学生来说是很大的隐性优势。有些同学会用若依这类快速开发框架改一改。不是说不行但我更建议自己从零搭一遍。因为毕设答辩时老师经常会追问登录鉴权怎么实现的路由守卫做了什么如果你只是在一个成熟框架上改页面这些问题很难答深。自己搭建虽然前期多花几天时间但换回来的是对整个技术栈更深的理解。2.2 版本选择与开发环境准备版本选型直接决定后面踩坑的数量我把自己实测稳定的组合列出来组件推荐版本备注JDK1.8 或 11兼容性和资料最多8最稳Maven3.8.x配阿里云镜像Spring Boot2.7.x不用3.x部分组件兼容麻烦MyBatis-Plus3.5.x分页和代码生成好用MySQL8.0 或 5.78.0需注意连接串参数Vue2.6.x配合Element-UI最成熟Element-UI2.15.x组件丰富文档齐全Node.js14~16新版也能跑但14最省心后端开发工具我用的是IntelliJ IDEA前端用VS Code数据库客户端用Navicat。这些属于个人习惯不强求。JDK、MySQL、Node安装的时候一路默认就行唯一提醒一点Maven别用IDEA自带的那版单独装一个并配好阿里云镜像否则下载依赖能等得你怀疑人生。2.3 前后端工程目录规划项目结构上我按前后端分离分成两个独立目录server和web。后端采用标准的Spring Boot分层架构server/ ├── src/main/java/com/dormitory/ │ ├── controller/ # 接口层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis-Plus的Mapper接口 │ ├── entity/ # 数据库实体 │ ├── dto/ # 数据交互对象 │ ├── config/ # 配置类跨域、MyBatisPlus、拦截器 │ ├── utils/ # JWT、日期处理等工具 │ └── common/ # 统一返回结果、异常处理 ├── src/main/resources/ │ ├── mapper/ # MyBatis的XML文件 │ └── application.yml前端目录用Vue CLI默认结构再加几个目录web/ ├── src/ │ ├── api/ # 按模块封装的请求接口 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── store/ # Vuex状态管理 │ ├── views/ # 页面组件 │ ├── utils/ # request.js、auth.js等工具 │ ├── App.vue │ └── main.js分层清晰的好处是调试时可以快速定位问题答辩时也方便讲清楚每一层承担的责任。2.4 数据库设计数据库是整个系统的核心地基建议表结构一次设计好不然后期改表能让人崩溃。我按下面的结构建库建表字段名采用下划线风格统一加注释CREATE DATABASE dormitory_system DEFAULT CHARSET utf8mb4; -- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50) COMMENT 姓名, role TINYINT COMMENT 角色 1管理员 2宿管员 3学生, phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 楼栋表 CREATE TABLE building ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 楼栋名称, manager VARCHAR(50) COMMENT 楼栋管理员, floor_count INT COMMENT 楼层数, remark VARCHAR(255) ); -- 宿舍表 CREATE TABLE dormitory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_id BIGINT NOT NULL COMMENT 所属楼栋, room_no VARCHAR(20) NOT NULL COMMENT 房间号, floor INT COMMENT 所在楼层, bed_count INT DEFAULT 4 COMMENT 床位数, used_count INT DEFAULT 0 COMMENT 已住人数, status TINYINT DEFAULT 0 COMMENT 0空置 1部分入住 2已满, area DECIMAL(5,2) COMMENT 面积, UNIQUE KEY idx_room (building_id, room_no) ); -- 入住记录表 CREATE TABLE checkin_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 学生用户ID, dormitory_id BIGINT NOT NULL COMMENT 宿舍ID, bed_no VARCHAR(10) COMMENT 床位号, checkin_time DATETIME NOT NULL COMMENT 入住时间, checkout_time DATETIME COMMENT 退宿时间, status TINYINT DEFAULT 1 COMMENT 1在住 0已退宿 ); -- 报修表 CREATE TABLE repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, dormitory_id BIGINT NOT NULL, content VARCHAR(500) COMMENT 问题描述, image_url VARCHAR(255) COMMENT 图片地址, status TINYINT DEFAULT 0 COMMENT 0待处理 1处理中 2已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 水电费表 CREATE TABLE utility_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dormitory_id BIGINT NOT NULL, bill_month VARCHAR(7) COMMENT 月份如2025-10, water_usage DECIMAL(8,2) COMMENT 用水量(吨), electricity_usage DECIMAL(8,2) COMMENT 用电量(度), total_fee DECIMAL(10,2) COMMENT 总费用, is_paid TINYINT DEFAULT 0 COMMENT 是否已缴纳, update_time DATETIME DEFAULT CURRENT_TIMESTAMP );这几张表基本涵盖了公寓管理的核心业务。注意我特意在dormitory表里加了used_count字段目的是查询房间列表时直接按这个字段过滤避免去入住记录表里做聚合性能好很多。实际写入的时候要保证顺序先检查余量再插入入住记录最后更新used_count。这个更新必须放在同一次事务里。注意宿舍表的status字段不用人为维护可以通过used_count和bed_count的对比自动推导。最稳妥的做法是在查询列表时用SQL表达式生成状态避免多处更新状态字段导致的数据不一致。3. 后端核心功能实现从登录鉴权到业务接口3.1 统一响应结构和异常处理第一步先把基础设施搭好。一个前后端分离项目里前端需要一套统一的响应格式来判断请求成功还是失败。我定义的返回结构很简单public class ResultT { private Integer code; // 200成功其他为失败 private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message 操作成功; r.data data; return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.code 500; r.message msg; return r; } }前端axios拦截器里判断code字段非200统一弹错误提示。全局异常处理用RestControllerAdvice把参数校验异常、业务异常、运行时异常区分开避免一报错就返回一堆看不懂的堆栈信息给前端。这块做好了后面调试联调会省很多时间。3.2 JWT登录鉴权登录接口不复杂核心逻辑三步根据用户名查用户→用BCrypt校验密码→校验通过生成JWT令牌。这里我踩过一个坑一开始用MD5做密码加密写起来是简单但答辩老师一定会问安全性问题后面换成了BCrypt。Spring Security里自带BCryptPasswordEncoder单独引入也行。关键代码如下PostMapping(/auth/login) public ResultLoginResult login(RequestBody LoginDTO dto) { User user userService.findByUsername(dto.getUsername()); if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } if (user.getStatus() 0) { return Result.error(账号已被禁用); } // 生成JWT7天过期 String token JwtUtil.createToken(user.getId(), user.getRole()); LoginResult result new LoginResult(); result.setToken(token); result.setRole(user.getRole()); result.setRealName(user.getRealName()); return Result.success(result); }JWT工具类里负责生成和解析令牌密钥写在配置文件中过期时间设为7天。每次请求通过拦截器校验Header里的Authorization解析出userId和role放到ThreadLocal里后续接口直接取当前用户信息不用反复查数据库。3.3 宿舍分页查询与条件过滤宿舍列表是后台使用频率最高的接口。我用MyBatis-Plus自带的分页插件同时支持按楼栋、按状态、按房间号模糊筛选GetMapping(/dormitory/page) public ResultIPageDormitoryVO page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long buildingId, RequestParam(required false) Integer status, RequestParam(required false) String roomNo) { IPageDormitoryVO result dormitoryService.queryPage(pageNum, pageSize, buildingId, status, roomNo); return Result.success(result); }Page对象最终返回给前端时需要包含total、records、current、size几个字段。Element-UI的分页组件可以直接对接。这里有个细节前端传入的pageNum和pageSize要做上限校验防止有人调接口时传一个10000的pageSize把数据库拖垮。我一般在拦截器层或Service层做了限制。3.4 入住与退宿的事务控制入住登记是系统里逻辑最严谨的功能之一用到了事务。我打开Transactional整体流程在一个方法里完成Transactional(rollbackFor Exception.class) public void checkin(CheckinDTO dto) { Dormitory dorm dormitoryMapper.selectByIdForUpdate(dto.getDormitoryId()); if (dorm null) { throw new BizException(宿舍不存在); } if (dorm.getUsedCount() dorm.getBedCount()) { throw new BizException(该宿舍已住满); } // 判断该学生是否已经在住 Long existing checkinRecordMapper.selectInUseCount(dto.getStudentId()); if (existing 0) { throw new BizException(该学生当前已在住不能重复入住); } // 插入入住记录 CheckinRecord record new CheckinRecord(); record.setStudentId(dto.getStudentId()); record.setDormitoryId(dorm.getId()); record.setBedNo(dto.getBedNo()); record.setCheckinTime(new Date()); record.setStatus(1); checkinRecordMapper.insert(record); // 更新宿舍已住人数 dormitoryMapper.increaseUsedCount(dorm.getId()); }selectByIdForUpdate是带行锁的查询目的就是保证并发场景下两个请求同时办理入住不会超卖床位。课设阶段可能不会有人恶意高并发压测但作为专业习惯我把这个点写进去了这也是答辩时展示系统严谨度的好素材——有经验的老师听到行锁这个词印象分会上去不少。退宿逻辑和入住对称把记录状态改成已退宿、标记退宿时间、宿舍已住人数减一。同样需要事务包裹。3.5 统计报表怎么聚合统计看板要展示的数据一般是各楼栋总房间数、已住人数、入住率、当月报修数量。最直观的方式是单独写一个统计Mapper用SQL聚合而不是在Java里循环计算。示例SQLselect idselectBuildingStats resultTypejava.util.Map SELECT b.id, b.name AS building_name, COUNT(d.id) AS total_rooms, SUM(CASE WHEN d.used_count 0 THEN 1 ELSE 0 END) AS used_rooms, COALESCE(SUM(d.used_count), 0) AS total_students, ROUND(COALESCE(SUM(d.used_count), 0) * 1.0 / COALESCE(SUM(d.bed_count), 0) * 100, 1) AS occupancy_rate FROM building b LEFT JOIN dormitory d ON b.id d.building_id GROUP BY b.id, b.name ORDER BY b.id /select这里用了LEFT JOIN而不是INNER JOIN原因是即使某栋楼还没有录入房间也要在统计结果里显示一条楼栋数据入住率显示0。这种边界考虑在真实项目里很重要也是加分项。4. 前端页面开发与前后端联调实战4.1 Vue项目初始化和UI组件库前端我用Vue CLI创建项目选了Vue Router和Vuex。UI库选择Element-UI虽然它的维护状态趋于平稳但在课设和内部系统场景下完全够用且组件种类、中文文档、网上案例都非常丰富。Element-UI建议按需引入否则打包出来的js体积会大得离谱。在main.js里这样处理import Vue from vue import App from ./App.vue import router from ./router import store from ./store import { Button, Input, Table, TableColumn, Dialog, Form, FormItem, Select, Option, Pagination, Message, MessageBox, Tag, Card, Menu, Submenu, MenuItem, DatePicker, Upload } from element-ui Vue.use(Button) Vue.use(Input) // ...其余组件 Vue.prototype.$message Message Vue.prototype.$confirm MessageBox.confirm按需引入时有一个容易踩的坑DatePicker、Select这类组件使用时依赖了内部指令和样式必须一并处理。我之前漏了样式引入导致下拉框点开没有选项排查半天。最稳妥是在项目里直接引入完整的element-ui样式import element-ui/lib/theme-chalk/index.css4.2 axios请求封装与登录态管理前端请求不能每个页面直接写axios.get(url)那样token注入、错误提示、状态码判断会写无数遍。我统一封装了一个request.jsimport axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }, error Promise.reject(error)) // 响应拦截器统一处理错误 request.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 error.response.status 401) { localStorage.removeItem(token) router.push(/login) } Message.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request登录态管理我直接用localStorage存token配合Vuex做用户信息缓存。很多教程会额外装vuex-persistedstate做持久化其实课设阶段不必引入那么多依赖localStorage手动同步就够了。4.3 路由守卫路由守卫是非常经典的加分点也在回答前后端分离怎么做权限控制这个问题时很关键。前端只能做显示层面的控制真正的安全拦截必须靠后端接口但前端路由守卫能显著改善体验防止未登录用户直接敲URL跳进后台页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path /login) { next() return } if (!token) { next(/login) return } // 角色菜单权限过滤 if (to.meta.roles !to.meta.roles.includes(Number(role))) { next(/403) return } next() })这里用到了路由meta里定义roles来控制访问权限比如管理员的报表路由只有role为1才允许进入。配合后端接口的权限注解形成双重校验。4.4 核心页面实现宿舍管理列表宿舍管理页面是整个前端最典型的CRUD页面。我用el-card包一层头部操作区搜索条件用el-form inline布局按钮区放新增宿舍批量导入中间是el-table底部是el-pagination。表头列依次是楼栋名称、房间号、所在楼层、床位数、已住人数、状态Tag、操作列。状态Tag我用不同颜色区分空置显示灰色或绿色部分入住显示蓝色已满显示红色。这个细节虽然简单但能让管理员一眼扫过去就知道哪些楼有空床位实际使用反馈很好。表格加载数据的方法async fetchData() { const res await request.get(/dormitory/page, { params: { pageNum: this.currentPage, pageSize: this.pageSize, buildingId: this.searchForm.buildingId || undefined, status: this.searchForm.status ? undefined : this.searchForm.status, roomNo: this.searchForm.roomNo || undefined } }) this.tableData res.data.records this.total res.data.total }注意查询参数里的空值一定用undefined而不是空字符串传过去。axios序列化时如果带了空的buildingId后端MyBatis-Plus会把它当成一个条件去匹配导致永远查不到结果。这是联调时最典型的一个看起来代码都对但数据就是不对的坑。入住登记页面我用一个Dialog弹窗里面套el-form。宿舍字段做成下拉框联动先选楼栋再选择宿舍宿舍列表只展示未满的房间。选完宿舍后自动带出床位信息。整个表单还要做前端校验比如学生名称必填、手机号格式合法防止无效数据直接打到后端。5. 打包部署与常见问题排查5.1 本地打包和联调开发完第一遍联调时前端直接npm run serve后端IDEA里启动两者通过代理解决跨域。Vue CLI的devServer配置如下devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }前端请求统一走/api前缀代理到后端的8081端口这样浏览器端看到的全是同源请求不用在后端加cors跨域配置。但如果前端单独部署在nginx、后端单独跑在一台服务器那还是需要后端处理跨域。我的经验是开发阶段用代理部署阶段再让后端加一个跨域配置类两头兼顾。后端打包没什么特殊IDEA里点package生成jar包启动命令java -jar dormitory-server.jar --server.port8081MySQL连接串如果使用MySQL 8.0需要显式指定时区否则会报连接超时或serverTimezone错误spring: datasource: url: jdbc:mysql://localhost:3306/dormitory_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai提示MySQL 8.0驱动类的名字是com.mysql.cj.jdbc.Driver不是之前的com.mysql.jdbc.Driver少写一个cj会直接启动报ClassNotFound。这个错在第一次搭建时非常常见。5.2 服务器部署思路课程设计和毕业设计一般在答辩时用本地演示就够但如果想要一个公网访问的地址部署也很简单。前端打包后生成dist目录里面全是静态文件交给nginx托管即可。后端jar包直接跑在服务器的Java环境上。一个简单的nginx配置思路server { listen 80; server_name your_domain; # 托管前端静态文件 root /usr/share/nginx/html; index index.html; # 解决前端刷新404找不到路由时回退到index.html location / { try_files $uri $uri/ /index.html; } # 反向代理后端接口 location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }5.3 常见问题排查速查表联调和部署阶段我遇到的典型问题整理成一张表方便对照排查现象可能原因解决办法前端所有请求404代理没配或proxy路径写错检查vue.config.js重启devServer请求返回401token过期或拦截器没带token重新登录检查请求拦截器写入逻辑登录接口报500数据库连接串没有时区参数按上文修改jdbc url中文乱码数据库字符集不是utf8mb4建库时指定utf8mb4连接串加characterEncodingutf8前端打包后页面一片白资源路径不对vue.config.js里publicPath改成./上传图片不能显示上传目录不在nginx托管目录把上传目录软链到nginx根目录宿舍列表状态为空status字段没有维护改用SQL推导状态不依赖冗余字段刷新页面404nginx没配try_files加上try_files $uri $uri/ /index.htmlMyBatis-Plus分页不生效缺少分页插件配置添加PaginationInterceptor新版是MybatisPlusInterceptor5.4 一个小细节账号密码与种子数据系统里必须内置一个管理员账号。我一般会在启动时插入初始数据密码统一用BCrypt加密后的字符串。为了方便演示可以在数据库初始化脚本里写上初始管理员账号admin、密码admin123宿管员账号house01学生账号stu01等。答辩演示前先想好演示路径优先展示新建楼栋→新增宿舍→入住登记→查看统计报表这条完整链路逻辑流畅又覆盖了核心模块。做完整套系统我个人最大的体会是公寓管理这类业务系统技术上没有什么高深莫测的算法难点全在细节和边界情况。比如并发入住时床位会不会超卖、退宿日期能否晚于入住日期、统计查询时null值怎么处理、权限控制前端和后端如何配合。把这些问题都考虑到了代码的健壮性自然就体现出来了。如果你正准备拿这个项目去做课设或毕设建议不要只关注能跑通这个底线多花点时间把权限逻辑、事务控制、异常处理讲清楚这些才是答辩时真正能给你撑场面、也让你在未来工作面试中说得出口的经验。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑