SpringBoot3+Vue.js3大学生租房管理系统实战指南
2026/9/13 16:58:37 网站建设 项目流程

简介:面向计算机专业毕业设计/课程设计的大学生们,这套基于 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房源表';

上面两张表里,sys_userpassword字段长度设为 64,因为 Spring Security 的 BCrypt 加密串长度就是 60 字符左右。house_infostatus用 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='看房预约表';

uk_house_user这个联合唯一键是个隐藏加分点。写代码时即使忘了判断重复预约,数据库层也会把数据挡下来,避免脏数据。在答辩时能主动说出这一层防重复设计,会比只写业务判断好很多。其他表如收藏表、评论表的结构更简单,这里不展开。

2.3 后端分层:Controller-Service-Mapper让代码不过期

数据表设计好后,需要对后端工程结构有一个清晰规划。我见过太多毕设项目把所有逻辑塞进 Controller 的一个方法里,虽然也能跑通,但后期改一个查询条件就要翻遍整个类,答辩被问“这里为什么崩溃”也很难定位。所以我建议按下面的包结构组织代码:

com.example.house ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo └── common

entity里放与数据库字段一一对应的实体;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 Initializr,Server URL 保持默认的https://start.spring.io,Java 版本选择 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这里要注意,MyBatis-Plus 的 starter 必须带spring-boot3后缀,否则会与 SpringBoot3 的自动配置机制冲突。mysql-connector-jscope设为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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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: auto

map-underscore-to-camel-case这个配置非常关键。数据库字段create_time会自动映射成实体的createTime,省去大量手动字段转换。serverTimezone如果不配 Asia/Shanghai,数据库连接经常报时区异常,这个坑几乎每年都能遇到。

统一返回结果类是前后端协作的基础。我一般会写一个泛型Result

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }

code=200表示成功,其他编码表示业务失败。前端只需要判断code是否为 200,不需要关心后台返回的字段名。如果后续要接入微信小程序,这种结构的适配成本也低。

3.3 房源列表分页与条件查询

房源列表是系统最核心的接口,通常需要支持分页、按状态筛选、按最高价格筛选。我会先用 MyBatis-Plus 的Page对象和LambdaQueryWrapper来实现,避免手写大量动态 SQL。

Controller 层的接口定义如下:

@RestController @RequestMapping("/api/house") public class HouseController { @Resource private HouseService houseService; @GetMapping("/page") public Result<IPage<HouseVO>> 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 IPage<HouseVO> pageQuery(int page, int size, Integer status, BigDecimal maxPrice) { Page<HouseInfo> p = new Page<>(page, size); LambdaQueryWrapper<HouseInfo> 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; }); }

eqle第一个布尔参数为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 LambdaQueryWrapper<HouseReservation>() .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 Result<String> 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 vite@latest house-front -- --template vue cd house-front npm install

--template vue会生成一个基础的 Vue.js3 单页应用。接下来安装路由、状态管理和 UI 组件库:

npm install vue-router@4 pinia element-plus @element-plus/icons-vue axios

Element 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 request

baseURL设为/api后,接口路径直接写/house/page就会自动加前缀,避免每个接口重复写完整地址。响应拦截器统一返回res.data,页面里拿到的直接是业务数据,不需要每次都写response.data.data。这里对网络热词没有依赖,只是常见的 Axios 封装方式。

4.3 发布房源页面的表单校验与图片上传

发布页面用 Element Plus 的el-formel-upload组件,关键代码:

<template> <el-form ref="formRef" :model="form" :rules="rules" label-width="80px"> <el-form-item label="标题" prop="title"> <el-input v-model="form.title" placeholder="请输入房源标题" /> </el-form-item> <el-form-item label="月租" prop="price"> <el-input-number v-model="form.price" :min="0" :precision="2" /> </el-form-item> <el-form-item label="封面图" prop="cover"> <el-upload action="/api/house/upload" :on-success="handleUploadSuccess" list-type="picture-card" > <el-icon><Plus /></el-icon> </el-upload> </el-form-item> <el-button type="primary" @click="submitForm">提交</el-button> </el-form> </template>

el-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) </script>

params里的展开操作会把searchForm中的maxPricestatus自动带上,Spring 后端通过@RequestParam接收。如果用户没有填写最高价格,后端就接收null,条件构造器会忽略该条件。分页数据由 MyBatis-Plus 返回,结构是标准化的recordstotal,前端直接解构使用即可。

状态渲染可以用标签组:

<el-tag v-if="item.status === 1" type="success">已上架</el-tag> <el-tag v-else-if="item.status === 0" type="warning">待审核</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能看到所有接口的说明。答辩时打开这个页面,老师能直观看到接口路径和方法描述。让你的项目不止能跑起来,还能清楚展示后端架构,信息量会更高。

本文还有配套的精品资源,点击获取

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

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

立即咨询