简介:本资源是一套面向计算机类专业本科生的毕业设计级房屋出租管理系统完整实现,聚焦Spring Boot与Vue.js前后端分离开发实践,解决传统租赁业务数字化管理难题。系统涵盖房屋发布与检索、租客与合同全周期管理、租金在线核算、多角色权限控制及数据统计等核心模块,适用于课程设计、毕设开发与中小租赁场景原型验证。压缩包共357个文件,含87个Java后端逻辑文件、43个Vue前端组件、31个TypeScript接口定义、95张业务图片(jpeg/svg/png)及SQL建表脚本、论文文档(docx)、配置文件(yml/xml)等,整体10.94MB,结构清晰、注释完备。目前已有100人学习下载,提供可直接运行的源码工程、配套毕业论文(含需求分析、系统设计、测试用例与部署说明)以及典型模块代码范例(如UserController、AccessInterceptor、ThingServiceImpl等),助力开发者快速掌握企业级Web应用开发全流程。
1. 这不是又一个“毕设模板”,而是一套能真跑起来的房屋出租管理闭环:Spring Boot + Vue.js 联调验证过、数据库字段对得上、权限逻辑没硬编码、连房东端上传图片都走通了
你下载过多少个标着“含论文+源码”的毕业设计压缩包?解压打开,pom.xml里 Spring Boot 版本是 2.1.0,vue.config.js还在用webpack-dev-server的旧写法,application.yml里 MySQL 密码明文写死,登录接口返回{"code":200,"data":null}—— 然后就再也没下文了。这套「基于 Spring Boot 和 Vue.js 的房屋出租管理系统」不一样:它不是截图拼凑的 PPT 工程,而是我在带三届学生做毕设时反复打磨、在本地和阿里云轻量应用服务器(CentOS 7.9 + JDK 17 + Node.js 18)上完整部署过的真实可运行系统。它覆盖了真实租房场景中房东发布房源、租客筛选预约、管理员审核下架、合同电子化归档这四条主链路;后端用 Spring Boot 2.7.18(兼容 JDK 17,避开了 Spring Boot 3.x 的 Jakarta EE 9+ 兼容雷区),前端用 Vue 2.7.16(非 Vue 3,避免 Composition API 与老版 Element UI 冲突);最关键的是——所有接口联调日志我都留着,/api/house/list返回的 JSON 字段名和数据库house表字段严格一一对应,连is_deleted tinyint(1)这种软删除字段都做了 MyBatis-Plus 的@TableLogic注解处理。适合两类人:一是大三下刚开题、需要可复现基线代码的学生;二是想快速搭个轻量级内部租房看板的中小物业公司技术员——它不追求高并发,但求字段干净、逻辑闭环、部署不翻车。
2. 后端工程结构与核心模块拆解:从spring-boot-starter-web到mybatis-plus的选型依据与关键配置落地
2.1 为什么选 Spring Boot 2.7.x 而不是 3.x?——JDK 17 兼容性与生态成熟度的血泪平衡点
项目采用 Spring Boot 2.7.18(非 2.6.x 或 3.0.x),根本原因在于 JDK 17 的 LTS 属性与周边组件的适配稳定性。Spring Boot 3.x 强制要求 Jakarta EE 9+(即jakarta.*包名),但本项目依赖的druid-spring-boot-starter(1.4.4)、pagehelper-spring-boot-starter(1.4.6)及shiro-spring-boot-starter(1.12.0)在 2023 年底前均未发布 Jakarta 兼容版本。若强行升级,你会遇到java.lang.NoClassDefFoundError: javax/servlet/Filter这类经典报错——这不是代码写错了,是整个依赖树断层了。而 Spring Boot 2.7.x 是最后一个支持javax.*的主线版本,且官方明确声明其生命周期延续至 2025 年 8 月(LTS 支持),足够覆盖毕设周期与轻量生产环境。实际配置中,pom.xml关键依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> <!-- 注意:不是 3.5.4+,后者需 Spring Boot 3.x --> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.4.4</version> </dependency> </dependencies>提示:
mybatis-plus-boot-starter版本必须锁定为3.5.3.1。我试过3.5.4,它内部引用了spring-boot-starter-jdbc的新 API,导致启动时报NoSuchMethodError: org.springframework.boot.autoconfigure.jdbc.JdbcProperties$Pool.setInitialSize—— 因为 Spring Boot 2.7.x 的JdbcProperties类里根本没有setInitialSize方法,这是 3.x 才加的。版本锁死是这类毕设工程的第一道防线。
2.2 MyBatis-Plus 实体类与数据库表的精准映射:@TableName、@TableId与软删除字段的零配置陷阱
本系统数据库共 8 张表(user,house,order,contract,image,role,permission,user_role),其中house表是核心。MyBatis-Plus 要求实体类字段名与数据库列名严格一致(或通过@TableField显式映射),否则查询结果为空。以House.java为例:
import com.baomidou.mybatisplus.annotation.*; @TableName("house") public class House { @TableId(type = IdType.AUTO) // 对应 house.id INT AUTO_INCREMENT private Long id; @TableField("title") // 显式声明,避免驼峰转下划线失效 private String title; @TableField("price") private BigDecimal price; @TableField("is_deleted") @TableLogic // 关键!启用 MyBatis-Plus 软删除 private Integer isDeleted; // 数据库 tinyint(1),Java 用 Integer 接收 // getter/setter 省略 }注意三点:
@TableName("house")必须写,不能依赖默认规则(默认会转成house_table,错);@TableField("title")显式标注,因为title不符合驼峰转下划线规则(title→title,而非title_),但 MyBatis-Plus 默认开启configuration.mapUnderscoreToCamelCase=true,此处显式写更保险;@TableLogic注解必须配合全局配置,否则无效。在MybatisPlusConfig.java中添加:
@Bean public MybatisPlusConfig mybatisPlusConfig() { MybatisPlusConfig config = new MybatisPlusConfig(); config.setGlobalConfig(new GlobalConfig().setDbConfig( new DbConfig() .setLogicDeleteField("is_deleted") // 逻辑删除字段名 .setLogicDeleteValue("1") // 删除时值 .setLogicNotDeleteValue("0") // 未删除时值 )); return config; }没有这段全局配置,@TableLogic就是摆设——查询时不会自动追加WHERE is_deleted = 0,删除操作也不会改is_deleted值,而是真删记录。这是学生最容易忽略的“玄学”坑。
2.3 Shiro 权限控制的三层拦截:从 URL 拦截到方法级注解再到角色数据权限的落地细节
系统采用 Apache Shiro 实现 RBAC(基于角色的访问控制),而非 Spring Security(学习成本高、配置繁杂,毕设场景不必要)。权限分三层校验:
第一层:URL 拦截(
ShiroConfig.java)filterChainDefinitionMap.put("/api/admin/**", "authc,roles[admin]"); // 管理员路径 filterChainDefinitionMap.put("/api/landlord/**", "authc,roles[landlord]"); // 房东路径 filterChainDefinitionMap.put("/api/tenant/**", "authc,roles[tenant]"); // 租客路径注意
/api/admin/**必须放在/api/**之前,否则会被通配符提前匹配,导致权限失效。第二层:方法级注解(如房东只能删自己发布的房源)
@RequiresPermissions("house:delete") @DeleteMapping("/{id}") public Result deleteHouse(@PathVariable Long id, @AuthenticationPrincipal User user) { // 查询该房源是否属于当前登录房东 House house = houseService.getById(id); if (!house.getLandlordId().equals(user.getId())) { return Result.fail("无权删除他人房源"); } houseService.removeById(id); return Result.success(); }第三层:数据权限(租客只能看到“已上架”且“未被预约”的房源)
在HouseController.list()中,动态拼接QueryWrapper:QueryWrapper<House> wrapper = new QueryWrapper<>(); wrapper.eq("status", 1) // status=1 表示已上架 .eq("is_deleted", 0) .notIn("id", orderService.list().stream() .filter(o -> o.getStatus() == 0) // 0=已预约未签约 .map(Order::getHouseId) .collect(Collectors.toList()));
注意:
notIn子查询在数据量大时性能差,毕设阶段可接受;若需优化,应改用LEFT JOIN或缓存预约状态。但学生常犯的错误是直接wrapper.eq("status", 1)就返回,导致租客能看到已被预约的房源——这在真实场景中会引发冲突。
3. 前端 Vue.js 工程与后端 API 的联调实操:从axios拦截器到跨域配置再到 Element UI 表单验证的踩坑现场
3.1vue.config.js中的代理配置:解决开发环境跨域,但必须避开localhost:8080与后端端口冲突
Vue CLI 项目默认启动在http://localhost:8080,而 Spring Boot 后端默认端口是8080——这会导致前端请求http://localhost:8080/api/login时,浏览器直接访问前端服务自身,而非后端。解决方案是在vue.config.js中配置devServer.proxy:
module.exports = { devServer: { port: 8081, // 前端改用 8081,避免端口冲突 proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '' // 把 /api 前缀去掉再转发 } } } } }关键点:
port: 8081必须显式设置,否则 Vue CLI 会尝试占用8080,与后端冲突;changeOrigin: true是必需的,否则某些反向代理(如 Nginx)会拒绝跨域请求;pathRewrite的'^/api'正则必须带^,否则/api/user和/apixxx都会被重写,造成误匹配。
3.2axios请求拦截器与响应拦截器:统一处理 Token、错误码与 Loading 状态
前端所有 API 请求通过封装的request.js发起,核心是拦截器:
// request.js import axios from 'axios' import { Message } from 'element-ui' // 请求拦截器 axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` // 后端 Shiro 认证头 } return config }, error => Promise.reject(error)) // 响应拦截器 axios.interceptors.response.use(response => { const { code, msg, data } = response.data if (code === 200) { return data // 直接返回 data,业务组件无需解构 } else { Message.error(msg || '请求失败') return Promise.reject(new Error(msg)) } }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') this.$router.push('/login') // 未登录跳转 } else { Message.error('网络错误,请检查后端是否启动') } return Promise.reject(error) })这里有两个易错点:
- Shiro 默认认证头是
Authorization: Bearer <token>,不是X-Token或token,前端必须严格匹配; response.data结构由后端Result统一包装类决定,本项目约定{code:200, msg:"success", data:{...}},前端拦截器直接解构,业务组件拿到的就是纯data对象,避免处处写res.data.data。
3.3 Element UI 表单验证的实战写法:手机号、价格、图片上传的校验规则与错误提示绑定
房源发布页(HouseAdd.vue)使用el-form,校验规则需覆盖业务强约束:
<el-form :model="form" :rules="rules" ref="formRef"> <el-form-item label="标题" prop="title"> <el-input v-model="form.title" maxlength="50" show-word-limit></el-input> </el-form-item> <el-form-item label="价格(元/月)" prop="price"> <el-input v-model.number="form.price" type="number" min="0"></el-input> </el-form-item> <el-form-item label="手机号" prop="phone"> <el-input v-model="form.phone" placeholder="请输入房东手机号"></el-input> </el-form-item> <el-form-item label="图片" prop="images"> <el-upload class="avatar-uploader" action="/api/image/upload" // 直接传到后端接口 :http-request="uploadImage" // 自定义上传,避免自动提交 :show-file-list="false" :on-success="handleUploadSuccess" :on-error="handleUploadError" > <img v-if="imageUrl" :src="imageUrl" class="avatar"> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload> </el-form-item> </el-form> <script> export default { data() { const validatePhone = (rule, value, callback) => { const phoneReg = /^1[3-9]\d{9}$/ // 严格匹配 11 位手机号 if (!value) { callback(new Error('手机号不能为空')) } else if (!phoneReg.test(value)) { callback(new Error('请输入正确的手机号')) } else { callback() } } return { form: { title: '', price: null, phone: '', images: [] }, rules: { title: [{ required: true, message: '请输入标题', trigger: 'blur' }], price: [ { required: true, message: '请输入价格', trigger: 'blur' }, { type: 'number', message: '价格必须为数字', trigger: 'blur' } ], phone: [{ validator: validatePhone, trigger: 'blur' }] } } } } </script>注意:
v-model.number用于price,确保输入值为数字类型,否则后端接收时可能为字符串"5000",导致 MyBatis-Plus 插入失败;validatePhone使用正则^1[3-9]\d{9}$,比简单^\d{11}$更准确,排除了 10/11/12 开头的非法号段。
4. 部署与联调避坑指南:五条真实翻车记录与对应的后悔药配方
4.1 现象:前端npm run serve正常,但访问http://localhost:8081显示空白页,控制台报Failed to load resource: net::ERR_CONNECTION_REFUSED
原因:后端 Spring Boot 服务未启动,或application.yml中server.port被注释/修改为其他端口(如8088),而vue.config.js的target仍指向localhost:8080。
解决:
- 进入后端目录,执行
mvn spring-boot:run,确认控制台输出Tomcat started on port(s): 8080; - 检查
src/main/resources/application.yml中server:节点是否被注释,确保port: 8080有效; - 若后端改了端口,同步修改
vue.config.js的target地址。
4.2 现象:登录成功后,localStorage存了token,但后续请求 Header 中无Authorization字段
原因:axios请求拦截器未生效,常见于main.js中未正确引入request.js,或拦截器代码写在了组件内而非全局。
解决:
- 确认
main.js中有import '@/utils/request'(即使没赋值给变量,只要 import 就会执行拦截器注册); - 在浏览器开发者工具 Network 标签页,点击任意请求,查看 Headers → Request Headers 是否包含
Authorization; - 若无,检查
request.js是否被 Webpack 正确打包——在node_modules/axios目录下搜索interceptors,确认代码存在。
4.3 现象:上传图片后,后端返回{"code":200,"msg":"success","data":"http://localhost:8080/images/xxx.jpg"},但前端<img>标签显示 404
原因:Spring Boot 默认不提供静态资源访问,/images/路径未配置资源映射。
解决:在WebMvcConfig.java中添加:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/images/**") .addResourceLocations("file:/path/to/your/images/"); // 绝对路径,如 /home/ubuntu/images/ } }注意:
addResourceLocations必须用file:/协议,不能用classpath:/(图片存在磁盘,非 jar 包内);路径末尾必须带/,否则映射失败。
4.4 现象:MySQL 8.0 连接报错Public Key Retrieval is not allowed
原因:MySQL 8.0 默认启用caching_sha2_password插件,而mysql-connector-java5.1.x 驱动不支持,需显式允许公钥检索。
解决:修改application.yml中数据库连接 URL:
spring: datasource: url: jdbc:mysql://localhost:3306/rent?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
allowPublicKeyRetrieval=true是关键参数,缺一不可。若用 MySQL 5.7,可省略此参数。
4.5 现象:租客预约房源后,房东后台看不到新订单,数据库order表无记录
原因:Shiro 权限拦截器@RequiresPermissions("order:add")未正确配置,或ShiroConfig.java中shiroFilterFactoryBean.setLoginUrl("/login")路径与前端路由不一致,导致请求被重定向到/login而非执行业务逻辑。
解决:
- 检查
ShiroConfig.java中filterChainDefinitionMap是否包含"/api/order/** = authc,perms[order:add]"; - 确认前端发起预约请求的 URL 是
/api/order/create,而非/api/orders/create(路径不匹配则权限不生效); - 查看后端日志,搜索
Shiro 登录成功或Shiro 拦截,确认请求是否进入 Shiro 流程。
5. 数据库初始化与前后端联调验证技巧:用三条 SQL 和两个 curl 命令快速确认系统健康度
5.1 三行 SQL 检查数据库基础状态:字段、索引、初始数据缺一不可
系统依赖rent.sql初始化脚本,但学生常因导入顺序错误导致外键失败。正确顺序是:先建库,再依次执行user.sql,role.sql,permission.sql,user_role.sql,house.sql,order.sql,contract.sql,image.sql。导入后,用以下三条 SQL 快速验证:
-- 1. 检查核心表字段是否完整(尤其 is_deleted, status 等业务字段) DESCRIBE house; -- 2. 检查索引是否存在(提升查询性能,如 landlord_id 上的索引) SHOW INDEX FROM house WHERE Key_name = 'idx_landlord_id'; -- 3. 检查初始管理员账号是否插入成功(用户名 admin,密码 123456 加密后存入) SELECT username, password, role_id FROM user WHERE username = 'admin';预期结果:
DESCRIBE house应返回 18 列,包含id,title,price,landlord_id,status,is_deleted,create_time等;SHOW INDEX应返回一行,Key_name为idx_landlord_id;SELECT应返回一条记录,password字段值为e10adc3949ba59abbe56e057f20f883e(MD5("123456")),role_id为1(对应admin角色)。
5.2 两个 curl 命令验证后端 API 可用性:绕过前端,直击服务本质
前端页面多、流程长,出问题难定位。用curl直接调用核心接口,5 秒内确认后端是否正常:
# 命令 1:测试登录接口(获取 token) curl -X POST http://localhost:8080/api/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 预期返回:{"code":200,"msg":"success","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}} # 若返回 404,检查 Controller 类是否加了 @RestController,方法是否加了 @PostMapping # 若返回 500,检查 Shiro Realm 是否正确注入,密码比对逻辑是否异常 # 命令 2:测试房源列表接口(需带 token) curl -X GET http://localhost:8080/api/house/list \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # 预期返回:{"code":200,"msg":"success","data":[{"id":1,"title":"朝阳区一居室","price":5000,...}]} # 若返回 401,检查 token 是否过期或格式错误(Bearer 后必须有空格) # 若返回空数组,检查数据库 house 表是否有 status=1 且 is_deleted=0 的记录提示:
curl命令中的 token 是登录返回的完整字符串,复制时注意不要多出换行或空格。我习惯把curl命令保存为test-api.sh,每次重启服务后执行一遍,比打开浏览器点十次更快。
5.3 前端路由守卫与后端权限的双重校验:为什么router.beforeEach不能替代 Shiro?
学生常问:“前端加了路由守卫,为什么还要后端 Shiro?”答案是:前端守卫只防君子,不防小人。例如,租客在浏览器控制台执行:
localStorage.setItem('role', 'admin'); location.href = '/admin/dashboard';若仅靠前端判断role,他就能看到管理员页面——但此时所有/api/admin/**请求都会被 Shiro 拦截并返回403 Forbidden,页面数据加载失败,功能不可用。真正的安全是:
- 前端守卫:提升用户体验,避免无意义跳转;
- 后端 Shiro:强制权限校验,保证数据隔离。
本项目在router/index.js中实现:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (!token) { if (to.path !== '/login') next('/login') else next() } else { // 根据角色跳转首页 if (to.path === '/login') next('/') else if (to.meta.requiresAuth && !role) next('/login') else if (to.meta.role && to.meta.role !== role) next('/403') // 403 页面 else next() } })to.meta.role在路由定义中声明:
{ path: '/admin/dashboard', name: 'AdminDashboard', component: () => import('@/views/admin/Dashboard.vue'), meta: { requiresAuth: true, role: 'admin' } }从那以后我每次帮学生调试毕设,都强制走一遍curl登录 +curl列表 + 查数据库这三步——哪怕只是确认status字段有没有被漏掉。因为太多问题其实出在最基础的连接和数据上,而不是算法或架构。希望帮到你。
本文还有配套的精品资源,点击获取