资讯动态

SpringBoot+Vue房屋租赁系统毕设实战:权限、状态机与订单结算

发布时间:2026/10/9 18:41:11 来源:尧图企业网站定制
简介一套基于 Spring Boot、Vue 与 MySQL 的房屋租赁系统毕业设计完整资料包面向计算机专业毕业生以及需要快速搭建同类项目的开发者可一站式解决系统开发、论文撰写、答辩展示等环节的核心需求。资源共879个文件压缩包约73.96MB其中Java与Vue源码对应前后端业务实现SQL脚本为数据库结构另含论文、答辩PPT、演示视频以及安装运行批处理脚本覆盖从部署到演示的完整链条。当前已有208人学习下载对于需要参考完整毕业设计流程的学生具有较高实用价值。通过该包可快速获取可运行的房屋租赁系统学习前后端分离开发模式并借助配套文档与视频理清设计思路、完成论文排版与答辩准备。同时文件内预留的留言板、房屋报修、合同信息、预约看房及我要当房东等模块也便于二次功能扩展。1. 毕业设计选型为什么这套“房屋租赁系统”值得复现毕业设计做管理系统最容易踩的坑是选一个“页面多但逻辑薄”的题目比如图书管理、班级管理最后做出来一个加了壳的CRUD答辩时讲不出深度。房屋租赁系统不一样它既有用户、房源、订单、收藏、公告五类核心数据也有“发布—审核—上架—签约—退租”的状态流转还涉及押金计算、权限控制、模糊搜索和图表统计。用SpringBoot做后端、Vue做前端、MySQL存数据正好覆盖主流技术栈。这套资源里源码、数据库脚本、论文、答辩PPT和演示视频是齐的核心价值在于拿过来能跑通改得动答辩时讲得清。适合第一次完整做前后端分离项目的学生也适合想快速搭一套系统参考的开发者。2. 系统架构与权限边界从角色划分到数据库表设计2.1 三个角色三种视图管理员、房东、租客的权限边界先梳理这套系统的角色。绝大多数房屋租赁毕设项目都会把用户分成三个角色管理员、房东、租客。注意不是“管理员、用户”两角色就完了那样设计会导致同一个用户既要发房源又要租房子数据归属混乱后面的统计报表也难写。三个角色的存在是数据模型和页面结构的前提。管理员管理用户状态、审核房源、处理投诉、查看平台统计数据房东发布房源、管理房源信息、查看租房申请、确认签约退租租客搜索房源、收藏房源、发起租房和退租申请、维护个人资料。从后端接口设计来看权限判断不能只看登录状态。房东修改房源时必须校验“这条房源是不是当前用户发布的”管理员审核房源时不能操作租客的收藏。常见做法是在Service层加一条归属校验用当前登录用户ID去查资源查到且归属匹配才放行否则直接抛403异常。只靠前端隐藏按钮不够用别人直接调用后端接口就能绕过。这套系统如果能在设计上体现出这个细节答辩时是加分项。2.2 数据库表怎么建核心表结构与字段设计要点数据表数量一般控制在7到10张左右比较合适。下面列出常见房屋租赁系统里的核心表和关系表名作用关键外键user用户表含角色字段无house房源表landlord_id → user.idhouse_image房源图片表house_id → house.idorders订单/租赁合同表house_id, tenant_idfavorite收藏表user_id, house_idnotice公告表无其中orders表最容易被简化掉。有的项目图省事只在房源表上加一个“已出租”字段结果退租记录没地方存押金明细也查不到。我的建议是只要系统涉及签约和退租就必须有一张订单表哪怕字段少一点也没关系。房源表的核心结构可以参考这段SQLCREATE TABLE house ( id INT NOT NULL AUTO_INCREMENT COMMENT 房源ID, landlord_id INT NOT NULL COMMENT 房东用户ID, title VARCHAR(100) NOT NULL COMMENT 房源标题, address VARCHAR(255) NOT NULL COMMENT 小区/地址描述, price DECIMAL(10,2) NOT NULL COMMENT 月租金单位元, area DECIMAL(8,2) DEFAULT NULL COMMENT 建筑面积单位平方米, type TINYINT DEFAULT 1 COMMENT 1整租 2合租, state TINYINT DEFAULT 0 COMMENT 0待审核 1已上架 2已下架 3已出租, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_landlord (landlord_id), KEY idx_state_price (state, price) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表;state字段是整个房源模块的核心。0到1是管理员审核1到2是房东下架1到3是租客签约成功。这几个状态要定死代码里最好用常量类或枚举管理不要散落成魔法数字。price字段必须用DECIMAL不要用FLOAT或DOUBLE否则页面上会出现99.999999这样的浮点误差。索引我一般会加组合索引(state, price)因为前台筛选条件通常是“上架状态的房源按价格排序”这个组合能同时支持过滤和排序。2.3 后端工程结构Controller-Service-Mapper 三层划分打开源码先看包结构。这套系统通常是一个Maven单模块工程下面分controller / service / mapper / entity / config / common六个包。入口类放在根包下启动时自动扫子包。entity数据库表对应的实体类字段和表字段一一对应mapper数据访问层项目里多用MyBatis-Plus继承BaseMapper就省去手写大部分SQLservice业务层房源审核、订单状态流转、押金计算都放在这里controller只做参数接收、调用service、封装返回结果config配置类跨域、拦截器、静态资源映射都放在这里common统一返回结果类、异常处理类、工具类。为什么强调这个结构因为答辩时老师可能会问“三层架构的好处”。回答不用背教科书就说一句话Controller今天可能被替换Service不会被替换因为业务规则才是系统的核心。把业务写在Controller里接口一多就变成大泥球。房屋租赁的业务规则集中在Service层下面是一段service方法骨架完成“租客发起租房申请”之后的校验逻辑Override public boolean applyRent(Long houseId, Long tenantId) { House house houseMapper.selectById(houseId); // 房源不存在或者已经租出直接拒绝 if (house null || !Integer.valueOf(1).equals(house.getState())) { throw new BusinessException(该房源当前不可申请); } // 自己不能租自己发布的房源 if (house.getLandlordId().equals(tenantId)) { throw new BusinessException(不能申请自己的房源); } Orders order new Orders(); order.setHouseId(houseId); order.setTenantId(tenantId); order.setStatus(0); // 0待房东确认 1已签约 2已退租 order.setCreateTime(LocalDateTime.now()); return ordersMapper.insert(order) 0; }三个判断的顺序有讲究先查房源存不存在再查状态再查归属逻辑。翻车案例往往把归属判断放在最前面结果房源不存在时先报“不能租自己的房子”异常信息会误导前端。MyBatis-Plus的selectById返回null要提前处理不要直接调用house.getState()否则就是空指针。3. 后端实现登录鉴权、房源上下架与订单结算3.1 JWT登录鉴权拦截器配置与token失效处理进入SpringBoot后端之后第一个要弄清楚的是登录鉴权。前后端分离的项目通常不用Session而是用JWT。登录接口成功后后端生成token返回给前端前端每次请求都在header里带Authorization。后端用一个拦截器统一校验token校验通过才放行。用SpringBoot实现拦截器分两步写一个实现HandlerInterceptor的类再注册到WebMvcConfigurer。常见写法是Component public class TokenInterceptor implements HandlerInterceptor { private static final String AUTH_HEADER Authorization; private static final String TOKEN_PREFIX Bearer ; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行浏览器预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(AUTH_HEADER); if (token null || !token.startsWith(TOKEN_PREFIX)) { response.setStatus(401); return false; } token token.substring(TOKEN_PREFIX.length()); // 解析token把用户id放进request attribute Claims claims JwtUtil.parseToken(token); if (claims null) { response.setStatus(401); return false; } request.setAttribute(userId, claims.get(userId)); return true; } }很多初次接触JWT的开发者会漏掉OPTIONS预检分支。浏览器在跨域请求发出前会先发一个OPTIONS请求这个请求不带业务token。如果拦截器不直接放行前端看到的错误是401而不是正常响应。JwtUtil是通用工具类内部用jjwt库解析和验签密钥配置在application.yml里。token过期时间建议设为2小时太短的话演示中间休息一会儿再刷新页面就会过期不好解释。3.2 房源发布与审核状态机流转的关键判断房源的state字段有四个值0待审核、1已上架、2已下架、3已出租。这个状态机是房源模块最核心的业务逻辑。发布、审核、上下架、出租对应四个动作每个动作都只允许特定的状态迁移房东发布房源初始状态0管理员审核通过状态从0到1管理员审核不通过状态从0到2并写入审核意见房东主动下架状态从1到2房东重新上架状态从2到1但要求房源未被签约过租客签约成功状态从1到3。代码里最忌讳的是“允许任意状态跳转”。比如房东在租客签约之后还能把房源下架会造成订单已签约但房源已下架的脏数据。实现状态机时可以在Service里用if判断“当前状态必须是目标动作的前置状态”。我一般会写一个stateCheck方法传入当前state和目标state不匹配就抛异常。租客签约动作还要额外校验订单状态不能重复创建订单。房源审核接口的Service层代码大致如下public boolean auditHouse(Long houseId, Integer targetState, String reason) { House house houseMapper.selectById(houseId); if (house null) { throw new BusinessException(房源不存在); } // 只有待审核状态才能被审核 if (!Integer.valueOf(0).equals(house.getState())) { throw new BusinessException(当前状态不允许审核操作); } house.setState(targetState); house.setAuditReason(reason); return houseMapper.updateById(house) 0; }这个接口的入参reason在targetState为1时可以为空但在targetState为2审核不通过时必须有内容否则前端展示审核意见时一片空白。业务校验放在Service层Controller只负责传参。另外targetState不能任由前端传任意数字后端要做白名单校验只允许1或2否则别人传个99数据库里就出现第四个状态。3.3 合同与押金订单结算的边界情况处理订单表是租赁系统的收尾环节。租客发起申请后房东确认订单状态从0变成1已签约系统同时把房源state改成3已出租。退租时订单状态变成2房源恢复为可租状态。押金计算是这个模块最容易被问到的细节。押金常见做法是纳入订单字段比如orders表里有deposit、rent、total字段。退租时通常遇到的问题是租期中途退租押金怎么退。不同项目规则不同毕设项目可以做简化但要在代码里体现规则可配置。例如退租结算逻辑用一个独立方法处理public BigDecimal calcDepositRefund(Orders order, LocalDate actualEndDate) { // 合同约定租期结束日 LocalDate endDate order.getEndDate(); // 正常到期退租全额退押金 if (!actualEndDate.isBefore(endDate)) { return order.getDeposit(); } // 提前退租扣一个月租金作为违约金 long months ChronoUnit.MONTHS.between(actualEndDate, endDate); if (months 1) { return order.getDeposit().subtract(order.getRent()); } // 不足一个月按一个月计算 return BigDecimal.ZERO; }这里“提前退租扣一个月租金”的规则是固定写死的商用系统会改成从配置表读取。BigDecimal运算后可能是负数所以return前要做max(0)保护。订单表里建议加endDate字段退租判断依赖合同结束日期只看签约日期算不出来。很多翻车案例就是订单表缺少租期字段退租逻辑没法写最后只能让用户手填退租日期。4. 前端实现Vue页面、路由守卫与接口联调4.1 页面架构与路由表按角色拆页面前端Vue部分通常是三个模块登录注册、前台用户界面租客视角、后台管理界面管理员视角。房东的操作往往和租客共用前台界面通过登录后的角色字段切换“我是房东”的操作入口。页面路由全部平铺在一份路由表里会显得混乱。常见做法是把路由拆成恒定部分和动态部分动态部分根据角色拼装菜单栏根据路由自动生成不用手写死菜单项。看源码时先找src/router/index.js这个文件决定了系统整体页面边界。毕设项目一般会有以下页面/login登录页支持管理员、房东、租客登录/home首页房源列表和筛选条件/house/detail房源详情页展示图片、价格、配套设施/user/profile个人中心展示我发布的房源、我的收藏、我的订单/admin/dashboard管理员数据面板。路由表的meta字段里一般会放roles数组标记这个页面允许哪些角色访问。前端只做页面级拦截真正的数据级权限还在后端接口控制。前后端都做才能算合格的权限设计。只看前端或者只看后端都会漏一边答辩时老师追问权限细节这样回答能自圆其说。4.2 axios封装与token注入接口联调的统一出口axios封装几乎是这套Vue项目的标配。在src/utils/request.js里创建axios实例设置baseURL然后在请求拦截器里从sessionStorage或localStorage取token注入请求头。这样每个接口调用不用手动带token代码会干净很多。import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const token sessionStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; } return config; }); request.interceptors.response.use( response { const res response.data; if (res.code 401) { sessionStorage.removeItem(token); window.location.href /login; return Promise.reject(new Error(登录已过期)); } return res; }, error { // 网络错误统一提示 return Promise.reject(error); } ); export default request;baseURL这里用了/api配合开发环境的代理转发。如果用vite在vite.config.js里配置server.proxy把/api开头的请求代理到后端端口。不要把后端地址写成完整路径否则部署时前端构建产物会写死域名换个环境就要重新编译。timeout设为10秒图片多的页面请求慢太短会误报超时。4.3 房源列表筛选与后台统计图表前台房源列表的筛选条件一般是这三个区域/小区关键词、价格区间、房型。在前端实现时不要用多个search输入框各自维护值最好用一个filter对象统一绑定。请求传参时把空字符串过滤掉避免后端收到一堆空参数。const filter reactive({ keyword: , minPrice: null, maxPrice: null, type: null, page: 1, pageSize: 10 }); function buildQuery() { return { keyword: filter.keyword || undefined, minPrice: filter.minPrice || undefined, maxPrice: filter.maxPrice || undefined, type: filter.type || undefined, page: filter.page, pageSize: filter.pageSize }; }非空判断放在前端而不是后端原因是后端MyBatis-Plus的包装器不好写“空字符串才过滤”。后端如果写成where keyword #{keyword}空字符串会导致keyword强制等于空搜不出结果。正确的后端写法是用StringUtils.isNotBlank判断前端则统一传undefined跳过查询参数。后台统计图表一般用ECharts的柱状图和饼图。数据源是管理员首页的三个接口每月房源新增量、各区域房源分布、订单状态占比。接口返回结构建议统一为{ name, value }数组这样前端图表组件能直接对接。柱子颜色、坐标轴这些参数不重要重要的是数据汇总的SQL要写对GROUP BY后面的字段和SELECT的字段要对应否则MySQL会报错或返回错误维度。提示如果npm install中途报错先检查node版本必要时用--legacy-peer-deps参数重装这能绕开大部分依赖冲突。5. 避坑指南跑通这套系统常见的五个关键坑5.1 MySQL 8驱动过期启动报ClassNotFoundException现象执行mvn spring-boot:run控制台报ClassNotFoundException: com.mysql.jdbc.Driver或者提示Failed to configure a DataSource。原因源码里如果application.yml还写着com.mysql.jdbc.Driver本地装的MySQL却是8.x版本。MySQL 8的驱动类名改成了com.mysql.cj.jdbc.Driver旧类名只在5.x时期有效。解决把配置改成下面这样spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/house_rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你自己的密码url里的serverTimezone参数不能丢。MySQL 8连接时不指定时区会报时区无法识别。密码改成自己root账号的密码。启动前先在数据库客户端或命令行里建好数据库再执行resources里附带的.sql脚本。我拿到源码后的习惯是先把application.yml三个变量的含义标出来再动任何代码。5.2 跨域配置带了凭证就不能用通配符现象前端页面能打开但点击登录按钮后浏览器控制台报CORS错误请求被阻塞。原因后端配置了允许跨域但用的是CrossOrigin或自定义过滤器允许来源写成*同时又把allowCredentials设成true。浏览器规定这两个配置不能同时存在请求会直接失败。解决跨域配置做一个WebMvcConfigurer的addCorsMappings指定允许的具体来源。如果本地前端跑在http://localhost:5173就这样写Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }开发阶段也可以直接用代理方式避免跨域vite或vue-cli的proxy配置里把/api转发给后端浏览器看到的请求是同源的CORS根本不会生效。演示时我建议用代理因为现场网络环境复杂代理方式最不容易翻车。5.3 路由守卫死循环token过期导致的跳转问题现象登录之后过一段时间刷新页面白屏。控制台报NavigationDuplicated或无限重定向。原因路由守卫里只判断了有没有token没判断token是否过期而且next(/login)写在所有情况的最前面导致在/login页面再次触发守卫。解决守卫逻辑要增加“to.path已经是/login”时的兜底router.beforeEach((to, from, next) { const token sessionStorage.getItem(token); if (to.path /login) { next(); } else if (!token) { next(/login); } else { next(); } });顺序很重要先判断to.path是不是登录页再判断token。如果顺序反过来token失效跳转登录页时守卫发现to.path不是login且token为空又跳一次login就会循环。这种死循环在答辩现场出现一次就够尴尬的提前把这段逻辑理顺能省很多事。5.4 图片上传后404静态资源映射没做现象图片上传接口返回成功页面也能正常显示但刷新页面后图片变成404。图片文件明明在磁盘或服务器目录里。原因SpringBoot默认不会把本地磁盘目录映射成可访问的URL路径。常见的项目里上传接口把文件保存到本地指定目录返回“/upload/123.jpg”给前端但SpringBoot不负责这个映射所以静态资源访问直接404。解决在后端config里加一个静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: System.getProperty(user.dir) /upload/); } }这样前端加载图片时请求/upload/123.jpg就会被映射到项目的upload目录。addResourceLocations的路径要以file:开头并且最后带斜杠。Windows和Linux写法略有差异如果部署到服务器要把路径改成绝对路径。5.5 答辩被问数据库索引不知道怎么答现象答辩演示时导师问“你在哪张表上建了索引为什么”答不上来。原因不能说“数据库自动建的”。主键索引是自动存在的但业务索引是手动加的。说不出来说明没认真看过SQL脚本。解决提前翻一下脚本看house表有没有idx_state_priceorders表有没有idx_house_id。回答的时候讲清楚房源列表的筛选条件是state和price联合索引能覆盖过滤和排序orders表按house_id查订单普通索引就够了。再准备一道追问比如“为什么不用前缀索引”回答价格和状态字段没有长文本不需要。我经手过的项目里凡是在索引上能讲出两句的答辩现场基本不会在这个问题上卡住。6. 从开发到答辩本地运行、演示脚本与可扩展方向6.1 本地运行三步走拿到资源后第一步不是改代码是先跑起来。把数据库导入、后端启动、前端启动三个环节拆开依次验证步骤操作成功标志1创建数据库执行sql目录下的脚本表和数据能正常查询2修改application.yml的数据库账号密码启动后端控制台出现启动成功日志3进入frontend目录npm install之后npm run dev浏览器能打开系统首页前端npm install如果卡住先检查node版本。这个项目的依赖锁文件决定它对node版本有要求版本过新或过旧都会安装失败。装完后看到本地访问地址就算成功。跑通之后不要急着改页面先把管理员、房东、租客三个账号的密码和角色分工弄清楚后面演示要用。6.2 答辩演示顺序答辩演示建议按“系统演示—业务深度—数据支撑”的顺序走。先展示登录和首页让评委看到完整界面接着选一条房源走完从发布、审核、申请签约到退租的完整流程重点展示状态变化然后打开后台统计页面用图表说明数据能汇总出运营指标最后翻开数据库按表讲一遍关键字段设计。之前辅导过A同学他就是按照这个顺序把状态流转和订单结算讲清楚老师只在数据库索引上追问了两句其他时间都在点头。6.3 再往前走一步如果时间充裕可以从两个方向给系统加厚度。一是支付模块把押金支付从普通状态流转升级成模拟支付扩展一个支付回调接口订单签约就变成“支付成功后才生效”。二是消息通知在订单状态变化时给用户写一条站内消息让前端右上角出现未读提醒。这两个方向都只要增加一两个表和接口工程量可控但答辩时能明显体现业务闭环意识。最后说一句实在话毕设源码的价值不在“跑起来”而在“改得动”。我经手过的项目中凡是能顺利通过答辩的都是先把数据库字段和状态流转吃透再动手改需求而不是拿着源码什么都不看就换模板。从那以后我每拿一套新系统第一件事永远是打开SQL脚本通读一遍表结构把每个state、status字段的含义全部标出来再去动代码。这套房屋租赁系统的结构不算复杂按这个流程走一遍很快就能上手希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑