每年一到毕业季,我就会被同一批题目包围:校园二手交易、图书管理系统、在线点餐,还有这个“基于Spring Boot的在线房屋出租系统”。说句实在话,这类题目在计算机类毕设里已经属于“经典款”,学校爱出,学生爱查,但真能把它讲明白、改顺手、扛得住答辩追问的,并不算多。标题里的Spring Boot、房屋出租、毕设源码这些关键词,拼出来的其实是一个非常典型的信息管理+交易流程类平台:房东发房源,租客找房源,平台做审核和订单管理。
我写这篇内容的原因很简单。网上去搜这个题目,能捞出一大把源码和文档,但大部分资源都是“能跑就行”,很少有人把里面的权限设计、状态流转、数据库取舍讲清楚,而这恰恰是答辩时老师最喜欢问的地方。这篇文章我就按这些年带毕设的经验,从需求拆解到落地实现,把每个关键点都过一遍,再穿插几个我在实际调试里容易翻车的坑。无论你是打算直接拿这套系统交差,还是想在此基础上加点自己的东西,这篇内容应该都能帮你少走不少弯路。
1. 项目概览与设计思路拆解
1.1 透过标题看清这个项目的真实需求
很多人看到“在线房屋出租系统”第一反应是:这不就是个发房源、看房源的网站吗?如果把需求理解到这个程度,项目做出来也就值一个及格分。真正拖垮这个题材的反而不是技术难点,而是逻辑上的“流程缺失”。
你要先理清这个系统里到底有哪些角色在干活。我一般会画一张三角关系图:平台管理员管审核和全局数据,房东负责发布房源和处理预约,租客负责搜索、预约、签约。如果还有普通游客角色,那游客只能看列表和详情,注册登录之后才能做进一步操作。
对应的功能需求大概有这么几块:
- 用户体系:注册、登录、个人信息维护、密码修改
- 房源模块:发布、编辑、上下架、审核状态管理
- 展示与检索:列表、详情、关键字搜索、区域筛选、收藏
- 预约流程:发起预约、房东确认、租客取消
- 订单模块:生成租赁订单、跟进状态、到期退租
- 后台管理:用户管理、房源审核、公告发布、数据统计
这些需求之间有强依赖关系。比如房源状态必须是“已上架”才能被搜索到,被预约和签约之后又不能继续被其他人下单。你要是把房源状态设计成一个随便改的字符串字段,后面写订单逻辑的时候必然会乱成一锅粥。
1.2 为什么这种毕设项目选Spring Boot最“稳”
现在很多高校指定Spring Boot作为毕设框架,不是因为它是所有方案里性能最强的,而是因为它恰好卡在“能学到东西”和“短期能做完”的中间地带。
从开发效率角度看,Spring Boot的自动配置把以前SSH时代那堆XML配置全砍掉了。以前配一个数据源要写半天,现在application.yml里几行搞定。内嵌Tomcat让项目打成一个jar包就能跑,省掉了单独装Tomcat、部署war包的步骤,这对毕设阶段来说太友好了。
从答辩角度看,Spring Boot生态里的组件名称评委基本都听过:MyBatis-Plus操作数据库、Redis做缓存、JWT做认证、Vue做前端页面。你只要能把每个组件解决什么问题说清楚,不用深入源码也能把答辩应付过去。
有一点要提醒你,别为了赶新潮直接上Spring Boot 3.x加JDK 17。虽然新版本确实更现代,但不少老电脑里装的是JDK 8,一些第三方依赖对新版本的支持也有兼容成本。如果学校没有强制要求,用Spring Boot 2.7.x加JDK 8是最稳的组合。做毕设的第一原则永远是“稳定跑通”,而不是“版本最新”。
1.3 功能模块怎么划分才不乱
很多同学的毕设代码拿到手之后,第一反应是想去改页面,但一打开项目就懵了。原因就是一个字:乱。UserController里堆了几百行,订单逻辑塞在里面,房源管理的代码也往里面挤。问题就出在模块划分没有按“角色”和“业务域”切开。
我比较推荐的做法是,后端按照角色和业务域拆出四条主线:
- 平台端接口:用户管理、房源审核、公告、统计
- 房东端接口:房源发布、编辑、预约处理、订单查看
- 租客端接口:房源浏览、收藏、预约、下单
- 公共接口:登录注册、上传、字典查询
对应到代码结构上,controller、service、mapper都是成组成组出现的,而不是一个UserController包打天下。你改房源逻辑就在HouseController和HouseService里改,改订单逻辑就在OrderController和OrderService里改。这样不仅读起来舒服,更重要的是答辩时老师问“订单模块在哪”,你能十秒钟内指给他看。
2. 技术选型与核心配置细节
2.1 一套能顺利跑起来的技术栈清单
网上同名项目成千上万,技术栈细节其实差异很大。我这套清单是基于最主流、最容易排查问题的组合整理的,你可参考着对照自己的项目:
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 稳定,JDK 8可跑 |
| ORM | MyBatis-Plus 3.5.x | 单表CRUD几乎不用写SQL |
| 数据库 | MySQL 8.0 | 免费、文档多、适合毕设 |
| 缓存 | Redis(可选) | 验证码、token可选方案 |
| 安全认证 | JWT + 拦截器 | 比Spring Security轻量好讲 |
| 工具库 | Lombok、Hutool | 简化代码,处理日期和文件 |
| 前端 | Vue 2 + Element-UI | 后台管理系统最常用的组合 |
| 构建工具 | Maven 3.6+ | 毕设项目的事实标准 |
要是你的项目里已经有Spring Security,那也别强行去掉,只要能说清楚认证鉴权流程就行。但如果你是在网上找的源码恰好用的是JWT加拦截器方案,我会更建议保留这个,因为拦截器的代码逻辑直观,答辩时三五句话就能讲完,不用背一堆过滤器链配置。
2.2 数据库表设计的几个关键决策
房屋出租系统的数据库是整个项目的灵魂,表结构设计不合理,后面每个功能都做得难受。下面这五张核心表基本是标配:用户表、房源表、房源图片表、预约表、订单表,另外可以再加一张公告表和管理员表。
先说最重要的房源表。我见过很多失败的作业,问题都出在字段设计上:
CREATE TABLE `house` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(120) NOT NULL COMMENT '房源标题', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图', `lease_type` tinyint NOT NULL COMMENT '1整租 2合租 3短租', `price` decimal(10,2) NOT NULL COMMENT '月租金', `area` decimal(8,2) DEFAULT NULL COMMENT '面积', `layout` varchar(50) DEFAULT NULL COMMENT '户型描述', `province` varchar(50) DEFAULT NULL, `city` varchar(50) DEFAULT NULL, `district` varchar(50) DEFAULT NULL, `address_detail` varchar(255) DEFAULT NULL, `landlord_id` bigint NOT NULL, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待审核 1上架 2已出租 3下架 4审核不通过', `description` text, `view_count` int DEFAULT '0', `del_flag` tinyint DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个决策要展开讲一讲。
金额字段必须用decimal(10,2),不能用float或double。因为钱涉及精度,float算出来的结果经常有误差,租客看到的订单金额少一分钱都会出问题。
地址要拆成省份、城市、区县、详细地址四个字段,而不是一个长长的address。因为搜索功能通常要按城市和区县筛选,拆开之后直接用等值查询就行。
状态字段用int做常量映射,在Java代码里定义枚举或常量类,不要直接在service里写魔法数字1、2、3。不然过两周自己回来看代码都不知道2代表什么。
房源图片单独建一张house_image表,不塞进房源表里的一个字段。一个房源要支持多图轮播,单独建表才方便,而且封面大图和详情小图可以复用同一张表。
预约表和订单表同样要注意状态字段。预约状态:0待处理、1房东确认、2租客已看房、3已签约、4已取消。订单状态:0待支付、1生效中、2已退租、3已取消。这些状态流转是有顺序的,你不能让一个“已取消”的预约直接跳到“已签约”。
2.3 application.yml 里最容易翻车的几个配置
很多项目拿到手后第一步启动就挂在配置文件上。不要小看这几行配置,我见过太多次因为时区问题导致查出来的时间差了八个小时。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/house_rent?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: global-config: db-config: logic-delete-field: delFlag logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB这里最容易被忽略的是serverTimezone=Asia/Shanghai。不配的话,数据库连接不是报错就是时间偏移八小时。新版本MySQL驱动要求显式写明时区,这行配置最好不动。
map-underscore-to-camel-case要设成true,不然数据库里的create_time字段映射不到Java类的createTime属性上,查出来的对象永远是一堆null。
mybatis-plus的logic-delete配置也很关键。用了逻辑删除之后,你调用HouseMapper的deleteById,实际执行的是update语句把del_flag置为1,数据不会真正丢。如果项目里的表没设计del_flag字段,这配置就不要乱开,否则所有SQL后面都会自动拼上del_flag条件,反而查不到数据。
3. 核心业务实现:用户认证、房源流转与订单闭环
3.1 登录认证到底要不要用Spring Security
每次有同学问我这个题目用不用Spring Security,我的回答都是:如果有现成源码且能跑通,那就保留;如果是从零手写,建议用JWT加拦截器的轻量方案。
为什么会这样?Spring Security功能强大,但概念多,过滤器链、UserDetailsService、SecurityContext这一套下来,很多同学自己写不明白。答辩时老师问一句“你的认证流程里请求是怎么被拦截的”,要是答不上来,反而露怯。
JWT方案你可以这样设计:
登录接口接收用户名密码,校验通过后生成一个token返回给前端。前端每次请求把token放在Header里,后端写一个拦截器统一解析token,把当前用户信息放到ThreadLocal里。
public class JwtUtil { private static final String SECRET = "your-secret-key"; public static String genToken(Long userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes())) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }登录接口:
@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.getOne(new LambdaQueryWrapper<User>() .eq(User::getUsername, dto.getUsername()) .eq(User::getPassword, MD5Util.md5(dto.getPassword()))); if (user == null) { return Result.error("用户名或密码错误"); } String token = JwtUtil.genToken(user.getId(), user.getRole()); return Result.success(token); }注意密码不要明文存库,简单做法是MD5加盐,更规范的做法是用BCrypt。虽然毕设阶段用MD5也能交差,但老师问起来,“为什么不推荐明文”这个问题你必须能答上来。
拦截器里的核心逻辑是:从Header里拿token,解析失败就直接返回401,解析成功就把userId和role放到请求上下文。部分接口还需要做角色校验,管理员接口只有role为admin才能访问。
3.2 房源检索的动态条件怎么优雅实现
房源的搜索条件通常是动态的:用户可能只输入关键字,可能只选城市,可能同时筛价格区间和租赁方式。这种场景最忌讳的就是手动用字符串拼接SQL,一不留神就拼出SQL注入漏洞。
用MyBatis-Plus的LambdaQueryWrapper可以非常清晰地解决动态条件拼接问题:
public IPage<HouseVO> searchHouse(HouseQueryDTO query) { Page<House> page = new Page<>(query.getCurrent(), query.getSize()); LambdaQueryWrapper<House> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(House::getStatus, 1); if (StrUtil.isNotBlank(query.getKeyword())) { wrapper.and(w -> w.like(House::getTitle, query.getKeyword()) .or().like(House::getDescription, query.getKeyword())); } if (StrUtil.isNotBlank(query.getCity())) { wrapper.eq(House::getCity, query.getCity()); } if (StrUtil.isNotBlank(query.getDistrict())) { wrapper.eq(House::getDistrict, query.getDistrict()); } if (query.getMinPrice() != null) { wrapper.ge(House::getPrice, query.getMinPrice()); } if (query.getMaxPrice() != null) { wrapper.le(House::getPrice, query.getMaxPrice()); } return houseMapper.selectPage(page, wrapper); }这里的eq、like、ge、le全部是带条件的,只有当传入值不为空才会拼接进SQL。既保证了安全性,又避免了写一长串if判断。
还有一点,列表页要用分页。MyBatis-Plus自带分页插件,配置好之后selectPage就会自动生成limit语句。别自己写PageHelper,也别一次性查出全表数据在内存里分页,房源数据量虽然不大,但演示时翻页卡顿很掉分。
3.3 预约看房与租赁订单的状态机
预约和订单是整个系统里最容易出逻辑漏洞的地方。我先把状态流转画出来,虽然不能画图,但用文字描述就是:租客发起预约(状态0)到房东确认(状态1),租客到现场看房后标记已看房(状态2),双方谈妥签约之后生成订单(状态3),流程走到这预约闭环才算结束。
这个过程中最容易犯的错是跳过房东确认环节。有的同学图省事,预约成功后直接生成订单,房东连确认都没有。你往大了说,这叫业务流程缺陷。老师一眼就能看穿。
订单生成时的并发问题也值得你提前思考。假设房源只有一套,两个租客同时点击“立即下单”,如果不加保护,两个人可能同时把同一套房源签下来。
解决办法很简单,核心思路是“条件更新”。不要把整条house记录查出来再判断状态,而是直接发一条带条件的update语句:
UPDATE house SET status = 2 WHERE id = #{houseId} AND status = 1执行这条SQL后,如果影响行数等于1,说明房源成功从上架状态改成了已出租状态,当前用户可以继续创建订单;如果影响行数等于0,说明别人已经抢先一步,直接提示“房源已被约走”。
这套方案不需要引入Redis分布式锁,对毕设项目来说已经够了。你需要在service里这样写:
boolean success = houseService.update(new LambdaUpdateWrapper<House>() .eq(House::getId, houseId) .eq(House::getStatus, 1) .set(House::getStatus, 2)); if (!success) { throw new BizException("房源已被预定,请选择其他房源"); }这条代码是你答辩时最能讲的点之一。你可以主动跟老师说:我在订单生成前用了条件更新来防止并发下重复租赁。就这一句话,比背十页业务功能介绍都管用。
3.4 双角色权限控制的隐藏坑
房屋出租系统里至少有房东和租客两个角色,加上管理员就是三个。前端会根据角色隐藏按钮,比如租客看详情页有“预约看房”,房东看自己的房源才有“编辑”和“下架”。但很多同学漏了后端校验,直接拿一个接口通吃所有角色,导致越权漏洞。
举一个高频案例:租客明明没有发布过房源,却直接请求一个删除房源的接口,传入对方的houseId,后端不做归属校验,结果就把别人的房源删了。
正确的后端校验思路是:操作型接口必须校验资源归属。房东修改或删除房源时,不仅要判断登录用户是房东,还要判断这条房源的landlordId是不是当前登录用户。不是就直接抛异常。
用一个简单注解加拦截器也完全可以做到。比如自定义一个@PreAuthorize("role"),拦截器解析出token里的角色字段,再对比目标路径需要的角色,不一致就拒绝访问。这个方案比引入Spring Security更直观,也更适合在答辩时摊开讲。
4. 实操过程:从源码导入到顺畅运行
4.1 拿到源码后的启动顺序与准备工作
网上下载的Spring Boot毕设项目通常是这样一套东西:一个后端文件夹、一个前端文件夹、一个SQL文件夹、一个README。不管你拿到的是什么样,启动顺序基本一致。
先准备环境:JDK 8及以上、Maven 3.6以上、MySQL 8.0、IDEA(或者Eclipse)。然后打开MySQL,创建一个空数据库,比如house_rent,再把SQL文件夹里的脚本导入进去。导入方式可以用Navicat的“运行SQL文件”,也可以直接在命令行执行source命令。
然后用IDEA打开后端项目,等Maven把依赖全部下载完。这个过程慢的时候可能要十分钟,别着急。接着修改application.yml里的数据库用户名和密码,一定要改成你自己机器上的实际值。
后端启动很简单,找到主类,类名一般是HouseRentApplication这种格式,右键运行。启动日志里没有异常,并且能看到Tomcat started on port 8080,说明后端部分已经起来了。
如果项目里还带前端,那通常是Vue项目。进入前端目录,执行npm install装依赖,再执行npm run serve启动开发服务器。然后浏览器打开localhost:8081之类的地址,通过前端页面的代理转发访问后端接口。
4.2 我每次调试都必做的三项检查
很多人反映项目明明按步骤操作了还是跑不起来。根据经验,90%的问题集中在下面三个地方,我每次拿到一套新源码都会先检查这三项。
先检查MySQL服务有没有启动。Windows上经常出现MySQL服务没开,Spring Boot启动时报错找不到数据库。打开服务管理器看一眼就能排除。
再检查数据库连接串里的用户名密码是否匹配。很多下载来的项目数据库密码是root或123456,你的本地MySQL可能设置了其他密码。配置不匹配时,日志里会出现Access denied for user。
最后检查前端代理转发配置。如果用的是Vue开发服务器,需要在vue.config.js里配置devServer的proxy,把/api开头的请求转发到后端的http://localhost:8080。很多人后端明明没问题,但前端页面一直报404,原因就是这个代理没配或者路径前缀不对。
4.3 二次开发值得动手改的5个地方
如果你想在答辩前给项目加一点“增量”,我推荐下面这几个方向,改动量从低到高排列,性价比极高:
- 首页加一个数据统计区,用ECharts展示房源总数、分区房源数量、订单趋势。后端提供聚合查询接口,前端画几张图,答辩时这块非常抓眼球。
- 给房源的发布时间和到期时间做提醒功能,租客后台显示合同即将到期。
- 上传房源图片时加一个图片压缩处理,避免大图直接存库导致加载慢。用Hutool工具类可以几行搞定。
- 预留公告模块的后台编辑器,把普通的文本域换成富文本编辑器。前端引入wangEditor,后端按HTML保存。
- 在做搜索时加入“最近浏览记录”,用Redis存一个列表,或者直接存数据库一张浏览记录表,能体现你考虑过用户体验。
选一个自己真正能讲清楚的功能做,不要贪多。毕设答辩忌讳“我每个功能都改了一点,但每个细节都说不明白”。
5. 常见问题与排查技巧实录
5.1 后端启动即报错:数据库连不上
这个报错是这类项目里出现频率最高的。常见现象是启动日志里出现Failed to configure a DataSource或者Access denied for user。
第一步,先用数据库客户端工具试着连接本地MySQL。如果能连接成功,说明是Spring Boot配置问题,重点检查application.yml里url、username、password这三项。如果客户端也连不上,那就是MySQL服务本身没启动,或者密码不对。
第二步,确认数据库名字是否正确。很多人导入SQL时建库叫house,配置文件里写的却是house_rent,或者反过来,启动必然报找不到数据库。把配置文件里的jdbc:mysql://localhost:3306/后面的库名改成你实际创建的库名即可。
5.2 项目启动成功,但页面白屏或者请求404
后端能启动,不代表前端一定能拿到数据。先打开浏览器的开发者工具,看Network面板里具体哪个请求返回了404。
如果请求路径是http://localhost:8081/api/house/list这种,那就先看8081端口是不是前端开发服务器。如果是前端服务器在处理这个请求,它会把/api开头的请求转发到后端8080。如果vue.config.js里没有配置代理,或者配置的target端口和实际后端端口不一致,就会404。
还要检查后端接口路径,有些控制器的映射是/api/house/list,有的则是/house/list。前后端只要错一个字,就会404。这个是联调阶段的常规坑,核对两边路径就行。
5.3 图片上传成功但前端加载不出来
这个问题很经典。图片上传到了本地磁盘文件夹,比如D:/upload/house/1.jpg,但前端页面访问http://localhost:8080/upload/house/1.jpg时返回404。原因很简单,Tomcat并不会默认把磁盘上的D:/upload目录暴露成URL。
解决办法是配置资源映射。Spring Boot里加一个WebMvcConfigurer实现:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/upload/"); } }这样前端就能用/upload/**路径访问到D:/upload目录下的文件了。别忘了路径末尾的斜杠,写错的话映射不生效。
5.4 订单时间和房源数据异常
不少项目会涉及查询某个月份的租金统计。如果发现统计数据对不上,先查数据库里的时间字段。常见问题是时间类型用varchar存储,排序和区间查询都会不对。统一改成datetime类型,并且Java实体类用LocalDateTime接收,前端格式化交给Jackson配置来处理。
还有状态字段脏数据的情况。比如房源状态被直接改成了2,但订单表里根本没有对应的有效订单,导致列表里显示“已出租”但实际不可查。这种情况多半是手工改了数据库。平时调试尽量通过系统的操作流程触发数据变化,不要直接改库里状态。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 启动报Access denied | 数据库账号密码错误 | 检查yml配置,和本地MySQL实际密码保持一致 |
| 启动报Unknown database | 库名写错 | 创建好数据库并改配置中的库名 |
| 前端页面404 | 前端代理没配好 | 检查vue.config.js的proxy配置 |
| 页面显示跨域 | 请求端口不一致 | 后端加CORS配置或全局跨域过滤器 |
| 图片打不开 | 本地磁盘路径未映射 | 配置addResourceHandlers |
| 数据查出来全是null | 驼峰映射没开 | 打开map-underscore-to-camel-case |
| 上传大图失败 | 上传大小超限 | 调大multipart相关参数 |
| 删除数据后列表还能看到 | 逻辑删除字段或配置缺失 | 检查del_flag和logic-delete配置 |
最后再分享一点实际经验
我做了多年毕设指导,发现真正能拿高分的项目,并不在于功能多花哨,而在于学生能不能把核心链路讲清楚。房屋出租系统看起来简单,但里面藏着用户认证、权限隔离、状态机流转、并发控制、文件映射这些硬知识点。你随便挑一个,比如预约状态为什么不能从0直接跳到3,比如并发下单怎么防超卖,只要你能说透,答辩就已经赢了一半。
我个人最推荐的做法是,拿到源码后先别急着改页面。花一个下午把数据库表关系画清楚,再用纸把租房全流程从注册到退租走一遍,标出每个状态变化是在哪个接口触发的。这个过程做完,你会发现这个系统在你眼里不再是一堆文件,而是一条完整的数据流。然后再动手去改,心里就有底了。做技术项目就是这样,先懂再改,永远比先改再懂省时间。