又到一年答辩季,我身边总有人问:SpringBoot+Vue+MySQL 的租房管理系统到底怎么才能又快又稳地做出来?说实话,这个题目绝对算得上毕业设计里的"顶流",每年都有一大批同学选它。原因也简单:业务对象明确、功能边界清晰、前后端都能展示,而且工作量刚好卡在一个学期能完成的范围内。
这篇内容不是贴一个标准答案式的"项目说明书",而是把我自己拆这类项目时常走的路、常掉的坑、答辩前一定要补上的细节,一路梳理出来。无论你是准备从零开发,还是已经写了一部分代码正卡在某个功能上,下面这几块内容应该都能帮你理出个清晰的路线:先说需求怎么圈定,再说数据库怎么设计,然后讲后端和前端要抓住哪些核心点,最后聊部署文档和答辩演示怎么收尾。
很多人觉得毕业设计就是"把代码跑起来",但真正拿高分的,其实是能把"为什么这么设计"讲清楚的人。这也正是这篇文章想帮你解决的。
1. 为什么租房管理系统是毕设选题里的"常青树":业务体量够用、工作量好控制
1.1 从"房东管房"到"租客找房",一个系统打通两端
租房这个场景,天然就有两条业务线:一边是房源供给方,也就是房东或中介,他们需要发布房源、修改信息、查看预约、确认签约、收租记账;另一边是房源需求方,也就是租客,他们需要浏览房源、收藏比较、发起看房预约、签约缴费、查看合同。
这两条线在传统线下模式里靠电话和纸质合同串联,而系统要做的,就是把它们全部搬上线,让每一步操作都有记录可查。
从这个角度看,你会发现租房管理系统和网上那些"XX信息管理系统"有本质区别:它不是简单一张表的增删改查,而是关联了大量主体和状态的业务系统。一套房源从发布到退租,中间要经历"空闲、被预约、已签约、已下架"的状态切换;合同签了之后,每个月又会产生账单,账单又有未缴、已缴、逾期这些状态。
正是因为有这些联动,这个题目才特别适合作为前后端分离项目的载体:后端要处理逻辑和状态流转,前端要处理多角色页面和交互,数据库要处理有外键关系的多表结构。做完你整个项目经历就丰满了,写简历、写论文都有东西可讲。
1.2 功能范围划到哪,"工作量性价比"最高
我见过不少同学做这个题目时最头疼的不是不会写代码,而是不知道该做哪些功能,不该做哪些功能。有人一开始就想搞聊天室、搞地图找房、搞小程序,结果进度拖到答辩前一周还在赶基础模块。
按毕业设计的标准来权衡,我建议把功能集控制在下面这个范围,性价比最高:
| 模块 | 核心功能 | 增值点 |
|---|---|---|
| 用户模块 | 注册、登录、个人信息修改 | 密码加密存储、头像上传 |
| 房源模块 | 房源发布、编辑、上下架、列表展示、详情查看 | 多图轮播、条件搜索 |
| 预约模块 | 租客提交看房预约、房东/管理员处理预约 | 时间选择、状态反馈 |
| 合同模块 | 自动生成租赁合同、合同列表、详情 | 到期提醒 |
| 账单模块 | 按合同生成账单、缴费状态管理 | 逾期标记 |
| 收藏模块 | 租客收藏房源、收藏列表 | 排序优化 |
| 公告模块 | 管理员发布公告、前台展示 | 置顶功能 |
| 统计模块 | 合同数、房源数、租客数等核心指标 | 图表展示 |
这里面,"用户、房源、预约、合同、账单"是绝对不能砍的骨架,因为环环相扣,论文的用例图、时序图都靠它们撑起来。收藏和公告属于锦上添花,做了能体现你的完整度,不做也不影响主线。
提示:功能范围一定在动手写代码之前就书面定下来。做系统最怕"边写边加需求",尤其是外部接入的那种,一会儿想加个评价功能,一会儿想加个退款流程,改到最后项目就跑偏了。
2. 数据库落地:表结构怎么分,决定了后面改代码的头疼程度
2.1 8张基础表,先把这个"骨架"立起来
数据库是整个项目的底盘,这块设计对了,后端的Mapper写起来会非常顺,前端的字段也基本能对齐。租房管理系统我建议至少建这8张表:
用户表(user)
存放管理员、房东、租客三类账号。关键字段包括:id、username(用户名)、password(加密后的密码)、nickname(昵称)、phone(手机号)、role(角色,我用1-管理员,2-房东,3-租客来标识,避免直接存字符串因为后续索引和判等都不方便)、avatar(头像路径)、create_time。
房源表(house)
核心大表。字段要有:id、landlord_id(房东用户ID,外键逻辑关联)、title(标题)、description(描述)、area(面积)、price(月租金)、address(具体地址)、cover_img(封面图)、status(房源状态)。这里我特别强调一种状态设计:0-待审核,1-已上架,2-已租出,3-已下架。把"已租出"单独作为一个状态非常重要,因为房子租出去之后就不该被再次预约了,但你也不能把它直接下架,否则合同和浏览记录对应不上。
房源图片表(house_image)
一个房源对应多张图片,所以单独建表。字段:id、house_id、image_url。毕业设计阶段用本地上传保存图片路径就够了,不需要接云存储,还能少一个需要讲解的外部依赖。
看房预约表(visit)
字段:id、house_id、renter_id(发起预约的租客)、visit_time(希望看房的时间)、remark(备注)、status(0-待确认,1-已确认,2-已完成,3-已取消)。这里的状态流转一定要做,"待确认 -> 已确认 -> 已完成"这个链路是你答辩时讲业务逻辑的好素材。
租赁合同表(contract)
字段:id、house_id、renter_id、landlord_id、start_date(合同开始日期)、end_date(合同结束日期)、rent(月租金)、deposit(押金)、status(1-履行中,2-已到期,3-已退租)。
账单表(bill)
字段:id、contract_id、amount(金额)、type(类型,租金或押金)、due_date(应缴日期)、status(0-未缴纳,1-已缴纳,2-逾期)。
另外还有收藏表(favorite)和公告表(notice),字段都比较简单,一个是user_id + house_id的唯一索引,一个是title + content + create_time。这两张表工作量不大,但能让前端页面丰富不少。
2.2 三种"省事写法"其实埋了大坑
光有表还不够,我见过不少项目因为建表图省事,后期坑到自己。最常见的三种:
第一,外键尽量少用或不用物理外键。很多初学者喜欢在 Navicat 里把外键关系拉满,每个关联字段都加物理外键。这样做的确能保证数据一致性,但带来的问题是:删除和改数据时各种外键约束冲突,项目跑起来后接口动不动报"外键约束失败"。我现在的做法是用逻辑外键——表之间有关系,但不建物理外键约束,靠代码控制引用关系。这样对上交项目来说更稳,代码里也不会有太多隐蔽的删除限制。
第二,时间字段千万别用 varchar 存储。有的同学图省事直接把日期存成"2025-06-01"字符串,等做"按月份筛选账单"或"按合同到期日排序"时就开始痛苦了。日期类型的比较、格式化、时区处理都比字符串可靠,所以start_date、end_date、visit_time、create_time一律建议用date或datetime类型。
第三,金额存储用 decimal,不能用 double。房租、押金、账单金额都是钱,double 在运算时会有精度丢失,比如0.1 + 0.2出来是0.30000000000000004。合同里每个月租金 2500 元,累计十几个账单再乘个系数,误差就会被放大。用decimal(10, 2)既精确又直观,前端显示也方便。
2.3 索引设计:别等数据多了才想起它
毕业设计规模不大,很多同学就完全放弃索引设计。但论文里若写到性能优化,索引是最容易拿出来讲的一点。我的建议是:
house表的status字段要建索引,因为首页房源列表最常按状态筛选。favorite表建(user_id, house_id)联合唯一索引,防止重复收藏。contract表的rental_id和house_id各建普通索引,合同查询和房源详情反向查合同速度会快很多。
实际上这些索引在数据量小的时候看不出什么差别,但你可以在论文"系统测试"部分写明:对一万条模拟数据做查询对比,加索引后响应时间从多少毫秒降到多少毫秒。这一段写出来非常加分。
3. SpringBoot 端实现:权限控制和列表查询最花心思
3.1 登录鉴权:先写拦截器,再谈框架
后端一上来就要解决的问题就是接口保护。如果所有接口都能裸调,那你整个系统的角色区分就没有意义了。我见过不少同学一开始就引入 Spring Security 加 JWT,配了半天还没跑通。这个题目真没必要上那么重的框架,一个拦截器加一个自定义注解,足够用了。
具体思路是这样:
登录成功后生成一个 token 返回给前端,token 里带着用户ID和角色。前端后续请求都在 Header 里带上这个 token。后端写一个拦截器,拦截所有以/api/**开头的请求,先把 token 取出来解析,解析成功就把用户信息放进请求上下文;解析失败直接返回 401。对于需要特定角色才能访问的接口,比如只有管理员能发布公告,就在 Controller 方法上用@RequireRole("ADMIN")注解,拦截器里先校验登录再校验角色。
核心代码大致长这样:
// 自定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String value(); } // 拦截器核心逻辑 public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (token == null) { throw new BizException(401, "未登录"); } LoginUser user = JwtUtil.parseToken(token); if (user == null) { throw new BizException(401, "登录已过期"); } // 检查方法上的角色注解 if (handler instanceof HandlerMethod) { RequireRole role = ((HandlerMethod) handler).getMethodAnnotation(RequireRole.class); if (role != null && !role.value().equals(user.getRole())) { throw new BizException(403, "无权限"); } } request.setAttribute("loginUser", user); return true; } }这套方案足够讲解,又不会在答辩时被老师追问到复杂度失控。如果你的项目用了 Redis,可以把 token 存到 Redis 里实现服务端登出,不加也没关系,属于优化项。
3.2 房源列表接口:分页、筛选、图片一次查齐
房源列表是整个系统里调用频率最高的接口,首页展示、搜索、房东管理房源、后台审核都用它。它的设计好坏直接影响前后端联调的体验。
我需要强调一个常见坑:不要把数据库表结构直接返回给前端。比如house表里存的是landlord_id,但前端详情页面要显示的是房东的联系电话,那你的接口就不能只返回原始字段。推荐的做法是定义一个HouseVO(视图对象),在里面额外加上landlordName、landlordPhone、imageList这些前端需要展示但数据库里没有的字段。
查询逻辑我用 MyBatis-Plus 来写,分页加条件构造器非常方便:
public PageResult<HouseVO> searchHouse(HouseQuery query) { LambdaQueryWrapper<House> wrapper = Wrappers.lambdaQuery(); wrapper.eq(query.getStatus() != null, House::getStatus, query.getStatus()); wrapper.like(StringUtils.hasText(query.getKeyword()), House::getTitle, query.getKeyword()); wrapper.between(query.getMinPrice() != null && query.getMaxPrice() != null, House::getPrice, query.getMinPrice(), query.getMaxPrice()); wrapper.orderByDesc(House::getCreateTime); Page<House> page = houseMapper.selectPage( new Page<>(query.getPageNum(), query.getPageSize()), wrapper); // 转 VO,并批量查询图片 List<HouseVO> voList = convertToVO(page.getRecords()); return new PageResult<>(voList, page.getTotal()); }这里有个性能细节挺值得写进论文:查询图片时不要在一个循环里去查数据库,每套房都查一次图片表,房源一多就会出现"1 + N"问题。应该在拿到所有房源ID之后,一次性查出所有图片,再按houseId分组,在内存里填充到对应的HouseVO里。代码写起来整洁,答辩时也能讲出一点性能优化意识。
3.3 合同与账单的状态流转:if-else 也要有章法
这个题目里最容易出 bug 的地方,就是状态没有闭环。比如:
- 租客提交预约后,房东确认了,房子的状态没有从"待租"改成"已被预约";
- 合同签完,房子状态还是"待租",结果首页上同一套房又被别人预约了;
- 合同到期了,账单状态没有自动标记为过期,一直挂在"未缴"里。
我建议你把状态流转当成一个状态机来写,在 Service 层里单独抽出方法,不要散落在各个 Controller 里。比如签约的方法,它要做的事是原子性的:
- 校验房子状态必须是"已上架";
- 生成合同记录,状态为"履行中";
- 把房子状态改成"已租出";
- 生成第一笔账单(押金加首月房租),状态为"未缴"。
这四个步骤缺一不可,而且最好用@Transactional包住,任何一个失败都整体回滚。同理,退租方法要做的事是:修改合同状态为"已退租",把房子状态改回"已上架",并把最后一个账单标记为已缴。把这些状态变更整理成一张表,就是论文里最好的业务逻辑说明。
| 操作 | 前置状态 | 后置状态 | 联动数据 |
|---|---|---|---|
| 租客提交预约 | 房源=已上架 | 预约=待确认 | 新增预约记录 |
| 房东确认预约 | 预约=待确认 | 预约=已确认 | 无 |
| 双方签约 | 房源=已上架 | 房源=已租出,合同=履行中 | 生成首期账单 |
| 合同到期 | 合同=履行中 | 合同=已到期 | 账单全部标记逾期 |
| 房东退租结清 | 合同=履行中/已到期 | 合同=已退租,房源=已上架 | 结清剩余账单 |
至于账单的"逾期"状态,可以写一个@Scheduled(cron = "0 0 0 * * ?")的定时任务,每天零点把已过应缴日期且未缴的账单改为逾期。这个功能不难,但体现了完整业务思考。
3.4 通用返回体与异常处理别漏掉
很多项目跑飞,不是因为主要逻辑不行,而是接口返回格式不统一。前端拿到一会儿{code: 200, data: ...}一会儿{success: true, result: ...},联调时疯狂写if判断,时间全浪费在这上面了。
我会在一开始就定义统一返回体:
public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> ok(T data) { ... } public static <T> Result<T> error(int code, String message) { ... } }再配一个全局异常处理器,把业务异常、参数校验异常、未知异常全部统一拦截,返回标准格式。这样前端的 axios 拦截器写一次就能全局处理错误。这块代码量不大,但对联调效率的提升是立竿见影的。
4. Vue 前端的页面组织:从"能打开"到"像那么回事"
4.1 三端页面与路由权限的搭建
前端部分最值得花时间的不是写静态页面,而是按角色组织路由和菜单。管理员、房东、租客三端看到的导航应该是不同的。我习惯在项目里用 Vue Router 加路由守卫,登录完成后把用户角色存到 localStorage,并在路由表的 meta 里标注role字段,然后在全局守卫里校验。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token) { next('/login') return } const role = localStorage.getItem('role') if (to.meta.role && to.meta.role !== role) { next('/403') // 角色无权访问的兜底页 return } next() })页面目录上,我建议按视图层划分,而不是按角色划分再复制三套页面:views/house/下放房源列表、详情、发布、编辑;views/contract/下放合同列表和详情;views/user/下放登录、注册、个人中心。菜单是否显示对应入口,用角色判断即可。这样复用组件很多,代码量反而小。
这里给你一个我自己使用的经验:每个页面组件一定拆成"列表 + 搜索表单 + 弹窗表单"三层结构,哪怕页面里只有一张表格,也把搜索区单独写成子组件。到了后面加筛选条件、加权限按钮时,你会感谢当时拆了组件。所有按钮的权限判断,也统一封装成自定义指令或者一个hasPermission()方法,而不是在每个按钮那里手写if (role === 'admin'),否则全局搜索"admin"能找到十几个地方,改权限逻辑就要改半天。
4.2 表单、搜索、图片上传的交互细节
交互细节决定老师点开系统时的第一印象。有四个细节我愿意单独拿出来讲。
第一,搜索区和表格必须联动。点击查询按钮时重新拉第一页数据,点击重置时清空所有搜索条件并刷新列表。很多同学做完搜索框不绑定事件,点了没反应,这是最容易暴露的硬伤。
第二,图片上传要能回显。后台上传房源图片时,前端要能预览、能删除,保存之后再次编辑时还能看到已上传的图片。如果图片无法回显,直接导致编辑房源时图片被清空,这是非常常见的 bug。用 Element UI 的el-upload时,file-list的绑定和:on-success回调里把返回值拼成完整路径,这块要反复测几遍。
第三,表格里的状态字段要转成中文标签。数据库里存的是0和1,前端表格直接显示0、1太丑了。我通常写一个formatStatus函数或一个全局过滤器,把数字映成中文字段加标签颜色:
const houseStatusMap = { 0: { text: '待审核', type: 'warning' }, 1: { text: '已上架', type: 'success' }, 2: { text: '已租出', type: 'info' }, 3: { text: '已下架', type: 'danger' } }第四,详情数据要"真"。很多项目为了演示方便,在房源详情页写死了一个房子数据,从列表点进去永远显示同一套房子。答辩现场老师随便点几个不同房源,发现内容一模一样,就露馅了。列表到详情的跳转一定要带id参数,详情页根据id拉接口。
4.3 联调时的"经典四坑"和排查思路
前后面联调是家常便饭,我整理四个高频坑,遇到问题先按这个方向排查:
第一个坑是跨域。前端在localhost:8080,后端在localhost:8088,请求直接发过去会被浏览器拦。解决办法有两个:后端配置 CORS 过滤器,或者前端配合vue.config.js里 devServer 的 proxy 代理。我个人更推荐开发阶段用 proxy,这样前端请求都走相对路径/api,部署到服务器时还能直接复用 Nginx 的反向代理,不需要改一行代码。
第二个坑是时间格式串了。后端的LocalDateTime序列化出来是一长串时间戳,前端el-date-picker根本显示不了。解决办法是在后端统一配置@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),或者全局配置 Jackson 的格式。
第三个坑是图片路径打不开。后端本地保存的图片路径是D:/upload/xxx.jpg,前端拿这个路径去访问当然是 404。正确做法是后端把图片保存到项目的静态资源目录或一个单独的上传目录,再通过/upload/xxx.jpg这样的相对路径访问,部署时由 Nginx 映射到实际磁盘路径。
第四个坑是 token 过期之后页面表现诡异。用户明明还在操作,突然所有接口都返回 401,前端如果不统一处理,就只能卡死在那里。在 axios 响应拦截器里判断code === 401,清空登录态并跳回登录页。这个处理做了,系统的健壮性观感会好很多。
5. 部署、论文和答辩演示:收尾工作比你想的重要
5.1 Linux 服务器上的部署组合:Jar 包 + Nginx
毕业设计做到了最后,如果只能跑在本地 IDEA 里,答辩时一旦换电脑或者网络环境变化就很容易翻车。稳妥的做法是把项目部署到你自己的服务器上,演示的时候打开公网地址就能用。
整体部署思路不复杂:
- 后端执行
mvn clean package -DskipTests,打出一个可运行的 jar 包; - 前端执行
npm run build,生成dist目录,里面全是静态文件; - 把 jar 包和数据库脚本
school_house.sql(我用"模拟租房平台库"这个名称)传到服务器; - 先用
mysql < school_house.sql导入数据库; - 运行
nohup java -jar school-house.jar > system.log 2>&1 &,后端启动; - 把
dist目录放到 Nginx 的 html 目录下,改写 Nginx 配置。
Nginx 配置我做了一个最简可用版本:
server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8088; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里把/api开头的请求全部转发给本地 8088 端口的后端服务,其他请求都走前端静态资源。前端里的baseURL直接设为/api即可。
注意:
try_files $uri $uri/ /index.html;这行是 Vue Router 的 history 模式必须的,不加的话刷新非首页路径会 404,可能让你在答辩时手忙脚乱。如果不想处理 rewrite,最简单的方案是用 hash 模式,但演示地址上会多个#号,这个看你自己取舍。
5.2 部署文档和操作手册要写成"给不懂代码的人看"
交付物里既然包含了部署文档,就别把它写成给自己看的随笔。老师在评阅时不一定有时间跑完整套部署流程,但文档的规范性会影响印象分。
我的部署文档固定包含这几部分:环境要求(JDK 版本、MySQL 版本、Node 版本)、导入数据库的具体步骤、后端启动步骤、前端启动步骤、常见问题表。尤其要写常见问题,比如"端口被占用怎么办""JDK 版本不匹配怎么办""数据库导入报错怎么办"。这都是在实操中遇到过的真问题,写进去显得项目完成度高,也方便你自已在答辩前回顾时少踩雷。
5.3 答辩演示路线的设计:三分钟讲清你的工作量
很多同学答辩失败不是因为代码不好,而是演示乱:一上来就点菜单,老师不知道这个系统里有什么门道。我会建议按一条贯穿业务闭环的路线来演示:
先切到管理员账号,展示系统首页的核心统计卡片(本月房源数、合同数、账单总额),然后新建一套房源并上传图片;切回租客账号,在列表中搜索刚才建的房源,点开详情,提交预约;切到房东视角,确认预约;重新回到管理员或房东视角,完成签约,生成合同和账单;最后展示账单列表,演示一笔缴费后状态从"未缴"变"已缴"。
这样三分钟左右,你既演示了三个角色的权限差异,又把房源、预约、合同、账单四张核心表串成了一条完整业务链。老师一眼就能看出这不是一个"纸面项目",是真的能跑通的闭环系统。
论文写作的时候,这条演示链路对应的数据流图要画清楚,时序图里把"谁触发、系统做什么、返回什么"标注完整。细节越扎实,被追问时你越好答。
6. 几个必须提前处理的"扣分雷区"
最后把我这些年看到的扣分点集中列一列,每个都真实发生过。
密码明文存储。不少项目的用户表password字段直接存明文,查数据库一眼看到,这是我见过最严重的系统安全问题之一。至少用 Spring Security 的BCryptPasswordEncoder或者自定义加盐哈希,把密码哈希之后入库。代码量不大,但能守住系统安全底线。
删除操作的连锁反应。如果你在系统里提供"删除房源"功能,删的时候房子名下还挂着未履约合同,直接删除会导致数据断裂。稳妥做法是做假删除,也就是给house表加一个is_deleted字段,普通delete改为逻辑删除,数据记录仍然保留在库里。这一点在论文数据库设计里讲出来也很加分。
演示数据不要全是"测试数据"。房源名称、地址千万别写"测试1""测试2"。导入数据库时,花点心思把房源标题做成"朝阳区两室一厅朝南出租"、合同租客名称做成"张先生",老师演示时看到的是完整的业务数据,观感完全不同。
答辩前清理垃圾代码和注释。我见过有人把排错时打印的System.out.println("1")都留在代码里,或者把大段写废掉的代码注释掉不删。提交前把无用的调试语句、死代码整段删掉,代码整洁是专业度的体现。
打包文件别漏掉数据库脚本。最后交付时,sql文件一定要跟着源码一起交,而且里面的建表语句要带上DROP TABLE IF EXISTS前缀,这样别人导入时不会因为表已经存在而报错。
这里想多说一句关于 "源码+数据库+论文+部署文档" 的配合思路:源码不管别人能不能跑起来,至少结构要自洽;数据库脚本一定是新建库、导入就能用的;论文里的数据库设计要和脚本完全一致;部署文档的每一步都要在干净环境里验证过。这四样东西互相印证,就是一份挑不出大毛病的毕设。
我在带人做这类项目时最常说的一句话是:毕业设计本质上是"可演示、可解释、能自洽"。功能可以少一点,但演示的时候不能卡壳;架构可以简单一点,但老师问到你为什么这么设计时,你得有完整的逻辑链条可讲。租房管理系统这个题目最大的好处就是业务天然成闭环,你只要把房源、预约、合同、账单这条主线做通,剩下的一切都是锦上添花。
如果你正准备动手写这个题目,建议先把上面的表结构和状态流转吃透,再按照"数据库 -> 后端接口 -> 前端页面 -> 部署"的顺序推进。这样每个阶段都有阶段性的产出,不至于到答辩前才手忙脚乱。后面如果遇到具体模块的具体报错,我们下篇再细聊。