SpringBoot3+Vue.js3大学生租房管理系统实战指南
简介面向计算机专业毕业设计/课程设计的大学生们这套基于 SpringBoot3 Vue.js3 MySQL8 的前后端分离租房管理系统是练习完整 Web 项目开发的理想案例。资源共 6 个文件、141.86MB包含 3 个源码压缩包、1 份 MySQL 数据库脚本、1 份操作录屏和 1 份需求文档分别用于代码部署、数据初始化、效果演示与需求梳理。已有 94 人学习下载。通过这套资料读者可以了解前后端分离架构下的接口设计与数据交互掌握 Vue3 组件化页面搭建、SpringBoot 服务编写及数据库表结构规划的实际流程也能参考其中房源发布、信息检索、后台管理等模块的完整实现配套录屏与文档能辅助快速跑通项目适合需要完整落地毕业设计或课程设计任务的学生借鉴。1. SpringBoot3 Vue.js3 大学生租房管理系统毕设选题为什么值得做前段时间有学弟问能不能做个“大学生租房管理系统”当毕业设计。看到这个题目时我的第一反应是它比普通的增删改查系统要有意思得多。学生租房不是电商下单它包含房源发布、审核、预约看房、收藏、线下签约这种状态流转正好能把 SpringBoot3 的后端校验、事务回滚、数据分页和 Vue.js3 的响应式渲染、前端路由、状态管理全部串起来。选择 SpringBoot3 加 Vue.js3 做组合在 2025 年已经不算激进而是常见的主流配置后端 JDK17 起步前端 Vite 构建两边都是独立工程很容易讲清“前后端分离”这个答辩高频概念。本文会从需求拆解开始把建表、IDEA 创建 SpringBoot3 项目、核心接口、Vue.js3 前端页面和后端联调、最后演示时注意的细节一次性讲清楚适合正在准备开题或者代码卡了一半的同学直接照着落地。2. 大学生租房管理系统的功能拆解与数据模型设计2.1 三种角色确定需求租客、房东、管理员拿到这种毕设题目第一步不该是去 IDEA 里新建项目而是先把“大学生租房”这个场景翻译成系统功能。我一般会把用户分成三种角色租客、房东、管理员然后按角色列功能。租客注册登录、按区域和价格搜索房源、查看房源详情、收藏房源、预约看房、查看预约状态。房东发布房源、修改房源状态、处理租客看房预约、查看自己的房源列表。管理员审核房东发布的房源管理用户状态查看基础统计数据。这里需要注意一个关键点很多同学习惯把“租房”做成直接下单支付订单但大学生租房的真实流程是通过平台获取联系方式先预约看房看完满意再线下签合同。所以我更推荐用预约状态机代替订单支付流程例如预约表的状态设置为“待确认—已确认—已完成—已取消”。这样既能避开支付接口申请的麻烦又更符合实际业务答辩时也可以解释成“为了降低系统复杂度把交易环节放在线下完成”。三种角色对应的核心数据关系可以整理成下面这张表角色核心功能对应数据表租客搜索房源、收藏、预约看房用户表、房源表、收藏表、预约表房东发布房源、处理预约房源表、预约表管理员房源审核、用户管理用户表、房源表从表里可以直观看出用户表与房源表是一对多关系房源表与预约表是一对多关系收藏表是用来连接租客和房源的多对多中间表。接下来建表时我只要围绕这几张核心表去扩展不会迷失在细碎功能里。2.2 数据表设计用MySQL建出可解释的ER模型我建议至少建 6 张表用户表、房源表、房源类型表、预约表、收藏表、房源评论表。其中评论表可以留到后续扩展但字段提前设计好后面加功能时不至于删表重来。下面给出核心表结构可以直接在 MySQL 8.0 中执行。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT BCrypt加密后的密码, role TINYINT NOT NULL DEFAULT 1 COMMENT 角色: 0管理员 1租客 2房东, real_name VARCHAR(50) COMMENT 真实姓名, phone VARCHAR(20) COMMENT 手机号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE house_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, landlord_id BIGINT NOT NULL COMMENT 房东ID, title VARCHAR(100) NOT NULL COMMENT 房源标题, house_type VARCHAR(20) COMMENT 户型如两室一厅, price DECIMAL(10,2) NOT NULL COMMENT 月租金, area DECIMAL(8,2) COMMENT 面积平米, address VARCHAR(255) NOT NULL COMMENT 小区地址, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0待审核 1已上架 2已下架 3已出租, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_landlord_id (landlord_id), KEY idx_status_price (status, price) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表;上面两张表里sys_user的password字段长度设为 64因为 Spring Security 的 BCrypt 加密串长度就是 60 字符左右。house_info中status用 TINYINT 而不是字符串是为了后续做状态筛选时查询效率更高idx_status_price是联合索引像“查询已上架且月租小于 1500 的房源”这种条件就能走索引排序也更快。预约表用于承载核心业务也顺手加一个唯一约束避免同一租客重复预约同一套房源。CREATE TABLE house_reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL COMMENT 房源ID, user_id BIGINT NOT NULL COMMENT 租客ID, visit_time DATETIME NOT NULL COMMENT 预约看房时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已取消 3已完成, remark VARCHAR(255) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_house_user (house_id, user_id, visit_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT看房预约表;uk_house_user这个联合唯一键是个隐藏加分点。写代码时即使忘了判断重复预约数据库层也会把数据挡下来避免脏数据。在答辩时能主动说出这一层防重复设计会比只写业务判断好很多。其他表如收藏表、评论表的结构更简单这里不展开。2.3 后端分层Controller-Service-Mapper让代码不过期数据表设计好后需要对后端工程结构有一个清晰规划。我见过太多毕设项目把所有逻辑塞进 Controller 的一个方法里虽然也能跑通但后期改一个查询条件就要翻遍整个类答辩被问“这里为什么崩溃”也很难定位。所以我建议按下面的包结构组织代码com.example.house ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo └── commonentity里放与数据库字段一一对应的实体dto放前端传过来的参数对象比如分页参数、预约参数vo放返回给前端的展示对象controller只做参数接收和结果包装service层写业务规则mapper层只负责 SQL 和 MyBatis-Plus 的数据访问。这样分层的直接收益是预约业务将来要从“写死状态”改成“可配置状态机”只需要改 Service 内的方法不需要动 Controller 的接口定义前端连地址也不用换。另外事务边界要放在 Service 实现类上而不是 Controller。预约成功后给房东发一条站内信如果站内信插入接口失败整个预约操作回滚消息不会发出去也不会出现“预约成功但房东没看到”的情况。这些内容在下一章实现接口时会具体落到代码上。3. 用IDEA创建SpringBoot3项目并实现核心后端接口3.1 IDEA创建SpringBoot3项目版本和依赖一次选对网上很多教程还在用老的 Spring Initializr 地址创建 Spring Boot 2.x 项目但 2025 年做毕设建议直接使用 SpringBoot3。在 IDEA 里创建 SpringBoot3 项目时我通常直接点新建项目选择Spring InitializrServer URL 保持默认的https://start.spring.ioJava 版本选择 17 或 21。需要注意SpringBoot3 最低要求 JDK17如果本机装的是 JDK8在 IDEA 里创建 SpringBoot3 项目时会直接报编译错误。创建时勾选这些依赖Spring Web、Validation、Lombok、MySQL Driver。MyBatis-Plus 不在这份依赖列表里需要生成项目后手动加入 pom.xml。不同数据库驱动名也不同SpringBoot3 对应的是com.mysql:mysql-connector-j。下面是核心 pom 依赖片段可以直接替换生成器给出的版本位置。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里要注意MyBatis-Plus 的 starter 必须带spring-boot3后缀否则会与 SpringBoot3 的自动配置机制冲突。mysql-connector-j的scope设为runtime表示只在运行时需要打包时也会带上但编译期不会引入避免代码顶层出现意外的包引用。3.2 配置application.yml和统一返回结果创建好项目后先把application.yml配好。下面是我常用的一份最小配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/student_house?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: automap-underscore-to-camel-case这个配置非常关键。数据库字段create_time会自动映射成实体的createTime省去大量手动字段转换。serverTimezone如果不配 Asia/Shanghai数据库连接经常报时区异常这个坑几乎每年都能遇到。统一返回结果类是前后端协作的基础。我一般会写一个泛型ResultData public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } }code200表示成功其他编码表示业务失败。前端只需要判断code是否为 200不需要关心后台返回的字段名。如果后续要接入微信小程序这种结构的适配成本也低。3.3 房源列表分页与条件查询房源列表是系统最核心的接口通常需要支持分页、按状态筛选、按最高价格筛选。我会先用 MyBatis-Plus 的Page对象和LambdaQueryWrapper来实现避免手写大量动态 SQL。Controller 层的接口定义如下RestController RequestMapping(/api/house) public class HouseController { Resource private HouseService houseService; GetMapping(/page) public ResultIPageHouseVO page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Integer status, RequestParam(required false) BigDecimal maxPrice) { return Result.ok(houseService.pageQuery(page, size, status, maxPrice)); } }defaultValue让前端不传分页参数时也能拿到第一页数据required false则表示状态和最高价是可选条件。这样面试官问“如果前端一个条件都不选怎么处理”时你的回答就是“接口默认返回所有已上架房源并按时间倒序排列”。Service 的核心查询逻辑Override public IPageHouseVO pageQuery(int page, int size, Integer status, BigDecimal maxPrice) { PageHouseInfo p new Page(page, size); LambdaQueryWrapperHouseInfo wrapper new LambdaQueryWrapper(); wrapper.eq(status ! null, HouseInfo::getStatus, status) .le(maxPrice ! null, HouseInfo::getPrice, maxPrice) .orderByDesc(HouseInfo::getCreateTime); return this.page(p, wrapper).convert(house - { HouseVO vo new HouseVO(); BeanUtils.copyProperties(house, vo); return vo; }); }eq和le第一个布尔参数为false时MyBatis-Plus 会忽略这个条件这就是条件构造器的核心。最后一个convert方法可以把持久层实体转换成前端需要的 VO 对象拷贝属性时BeanUtils.copyProperties会自动匹配同名字段。这样做的目的是避免把landlordId这类内建字段直接暴露给前端后续补充房东昵称时也能集中处理。3.4 看房预约接口事务和业务规则校验预约功能涉及资源校验和数据库插入需要放在 Service 层完成。Transactional(rollbackFor Exception.class) public void reserve(ReserveDTO dto) { HouseInfo house houseMapper.selectById(dto.getHouseId()); if (house null || house.getStatus() ! 1) { throw new BusinessException(400, 房源不存在或不可预约); } Long count reservationMapper.selectCount(new LambdaQueryWrapperHouseReservation() .eq(HouseReservation::getHouseId, dto.getHouseId()) .eq(HouseReservation::getUserId, dto.getUserId()) .eq(HouseReservation::getVisitTime, dto.getVisitTime())); if (count 0) { throw new BusinessException(40000, 该时段已预约请更换时间); } HouseReservation reservation new HouseReservation(); reservation.setHouseId(dto.getHouseId()); reservation.setUserId(dto.getUserId()); reservation.setVisitTime(dto.getVisitTime()); reservation.setStatus(0); reservationMapper.insert(reservation); }Transactional(rollbackFor Exception.class)表示任何异常都会触发回滚。如果预约成功后还要调用消息服务给房东发送通知而消息服务暂时不可用抛出了异常这行注解就能保证预约数据也不会落库避免数据不一致。业务校验放在数据库约束之前虽然数据库有唯一键兜底但提前返回友好提示比直接抛出 SQLIntegrityConstraintViolationException 更专业。BusinessException会被全局异常处理器转换成对应Result返回给前端。3.5 图片上传与静态资源映射发布房源必须支持上传封面图。简单做法是把图片保存到本地磁盘然后在配置类里做静态资源映射。PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; String basePath /data/house-images/; file.transferTo(new File(basePath fileName)); return Result.ok(/images/ fileName); }代码中UUID.randomUUID()是为了防止文件名重复避免不同房东传了同名图片导致互相覆盖。返回给前端的是一个相对路径/images/xxx.jpg而不是完整域名这样前后端部署时只要调整通用前缀即可。Spring 配置类需要重写addResourceHandlers把/images/**路径映射到本机路径。Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file:/data/house-images/); }这里最后必须加斜杠否则 Spring 可能解析不到目录。如果图片上传后访问 404优先检查这个配置。4. Vue.js3 前端项目创建与页面交互实现4.1 用Vite创建Vue.js3项目并集成Element Plus前端我通常使用 Vite 创建工程而不是 Vue CLI因为 Vite 在开发模式下启动速度快配置也更直观。创建命令如下npm create vitelatest house-front -- --template vue cd house-front npm install--template vue会生成一个基础的 Vue.js3 单页应用。接下来安装路由、状态管理和 UI 组件库npm install vue-router4 pinia element-plus element-plus/icons-vue axiosElement Plus 的按需导入可以简化使用但毕设项目我建议在main.js里全量引入省去自动导入配置的时间import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(ElementPlus) app.use(router) app.mount(#app)全量引入会让打包体积变大但开发效率最高。毕业设计阶段更看重功能完整度和演示流程不值得在按需导入上消耗太多调试时间。4.2 封装Axios请求统一请求前缀和Token注入前端页面会大量调用后端接口如果每个页面都单独写 axios后续遇到需要统一注入 token 的情况就得改很多文件。我会在src/utils/request.js中封装一个实例。import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.message) return Promise.reject(error) } ) export default requestbaseURL设为/api后接口路径直接写/house/page就会自动加前缀避免每个接口重复写完整地址。响应拦截器统一返回res.data页面里拿到的直接是业务数据不需要每次都写response.data.data。这里对网络热词没有依赖只是常见的 Axios 封装方式。4.3 发布房源页面的表单校验与图片上传发布页面用 Element Plus 的el-form和el-upload组件关键代码template el-form refformRef :modelform :rulesrules label-width80px el-form-item label标题 proptitle el-input v-modelform.title placeholder请输入房源标题 / /el-form-item el-form-item label月租 propprice el-input-number v-modelform.price :min0 :precision2 / /el-form-item el-form-item label封面图 propcover el-upload action/api/house/upload :on-successhandleUploadSuccess list-typepicture-card el-iconPlus //el-icon /el-upload /el-form-item el-button typeprimary clicksubmitForm提交/el-button /el-form /templateel-upload组件的action指向后端图片上传接口上传成功后回调函数里拿到后端返回的路径再赋值给form.cover。这里需要注意前端提交房源数据时封面字段保存的是/images/xxx.jpg这种相对路径图片预览则由后端拼接成完整地址。表单校验规则里价格字段可以添加类型校验避免用户输入负数或空值const rules { title: [{ required: true, message: 请输入房源标题, trigger: blur }], price: [{ required: true, message: 请输入月租价格, trigger: blur }] }4.4 房源列表页的分页、搜索和状态渲染房源列表页是整个系统的门面通常包含筛选栏、卡片列表和分页器。查询逻辑如下script setup import { onMounted, ref } from vue import request from /utils/request const page ref(1) const size ref(8) const total ref(0) const houseList ref([]) const searchForm ref({ maxPrice: null, status: 1 }) async function fetchHouseList() { const data await request.get(/house/page, { params: { ...searchForm.value, page: page.value, size: size.value } }) houseList.value data.records total.value data.total } function handleSearch() { page.value 1 fetchHouseList() } onMounted(fetchHouseList) /scriptparams里的展开操作会把searchForm中的maxPrice和status自动带上Spring 后端通过RequestParam接收。如果用户没有填写最高价格后端就接收null条件构造器会忽略该条件。分页数据由 MyBatis-Plus 返回结构是标准化的records、total前端直接解构使用即可。状态渲染可以用标签组el-tag v-ifitem.status 1 typesuccess已上架/el-tag el-tag v-else-ifitem.status 0 typewarning待审核/el-tag这一小段代码演示了 Vue.js3 条件渲染和 Element Plus 标签组件的结合。答辩时可以说状态不是写死文本而是由后端状态码映射到不同标签方便统一维护样式。5. 答辩演示排错CORS、404和状态混乱实战5.1 跨域问题前端代理与后端CORS同时配置前后端联调时遇到最多的问题就是跨域。开发阶段最推荐的方案是使用 Vite 代理在vite.config.js中配置export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端发出的所有/api请求都会由 Vite 开发服务器转发到 8080浏览器看到的是同源请求不需要后端开启 CORS。但如果你希望后端也能直接支持跨域可以在 SpringBoot3 里写一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(http://localhost:*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true); } }注意allowedOriginPatterns与旧的allowedOrigins不同它会允许带端口变化的本地地址在 SpringBoot3 中更实用。实际部署时建议只保留其中一种方案不要前端代理和后端 CORS 同时开启否则 Cookie 和 Header 会异常混乱。5.2 答辩演示最容易翻车的三个点404、日期格式和重复提交第一个容易翻车的点是静态资源 404图片能上传但访问不到优先检查静态资源映射路径末尾是否加斜杠以及file:后面的实际目录是否存在。第二个是日期格式不一致。前端传给后端visitTime时默认是2025-05-10T14:30:00这种 ISO 格式Spring 后端可能解析失败。我一般在前端提交前使用 dayjs 格式化为YYYY-MM-DD HH:mm:ss后端用DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss)接收。第三个是用户连点“预约”按钮产生重复提交。前端可以加按钮 loading 状态后端同时依赖唯一索引兜底。演示时快速双击按钮如果后端返回“该时段已预约”而不是堆栈异常会让答辩老师印象深刻。5.3 用接口文档和数据预置提升答辩观感毕业设计答辩时间有限老师不可能逐一操作所有功能。我建议在启动项目后预置一套演示数据三个角色账号、五套不同价位房源、一条待审核预约。这样演示时不需要现场注册和发布直接进入核心流程。另外SpringBoot3 项目可以快速接入 springdoc-openapi 接口文档依赖导入后访问/swagger-ui.html能看到所有接口的说明。答辩时打开这个页面老师能直观看到接口路径和方法描述。让你的项目不止能跑起来还能清楚展示后端架构信息量会更高。本文还有配套的精品资源点击获取