每年到了项目季,总有人拿着类似的标题来问我:基于SpringBoot+Vue的房屋租赁管理系统,到底能不能跑起来,好不好改。说实话,这种标题背后不只是一套代码,而是很多人课程设计、毕业设计或者第一次接触全栈开发时的完整工程样本。它的核心链路并不复杂:后端用SpringBoot+MyBatis+MySQL,前端用Vue全家桶,把管理员、房东、租客这三类角色的房源、合同、账单、报修业务串起来。这篇文章我会从业务功能、数据库设计、后端接口、前端页面到最终部署运行,把整套系统一层层拆开讲清楚。无论你刚拿到源码还没头绪,还是想基于它做二次开发,这篇应该都能帮你省下不少时间。
先说明一点,市面上叫“房屋租赁管理系统”的版本很多,页面和表结构可能略有差异,但业务主线和实现思路基本一致。我这边以“SpringBoot + Vue + MyBatis + MySQL”的主流版为例,讲的都是能直接落到代码里的东西。
1. 项目全貌:这套系统到底在解决什么问题
1.1 三类用户、四条业务主线
最开始别急着看代码,先把业务盘清楚。这系统不是简单的一张表CURD,而是围绕“租房”这个场景,做了完整的角色分工。
系统里一共有三类角色:
- 管理员:管人、管房、管合同。能看所有用户、审核房源、查看全部租赁记录、管理账单统计、发公告。
- 房东:把房源挂出来,查看租客申请,在线签约或拒绝,查看自己名下房源的账单、催缴租金。
- 租客:浏览房源、搜索筛选、收藏、在线发起租赁申请、查看/确认合同、缴纳账单、发起报修。
这套角色不是摆设,它决定了权限、菜单、接口都得分层。前端菜单要根据角色动态显示,后端接口也要做权限校验,否则一个普通租客直接调管理员的接口就能看到所有合同,整个系统就形同虚设了。
业务主线我习惯归纳成四条:
- 房源生命周期:从发布、审核、上架、被申请、签约出租到退租重新上架。
- 租赁主流程:租客看房、发起申请、房东确认、合同生效、到期续租或退租。
- 账单资金流:合同生效后生成租金/水电费账单,租客缴费,房东确认到账,管理员总览统计。
- 售后闭环:租客报修,房东或管理员处理,处理完成后更新状态。
我见过很多人看着代码不知道从哪看起,其实就是没抓住这四条线。你只要顺着“房源状态流转”和“合同状态流转”去读代码,整个项目很快就能串起来。
1.2 技术组合为什么是 SpringBoot + Vue + MyBatis
这个组合属于典型的“能打又不太重”的方案,放到2025年依然没有过时感。
SpringBoot负责后端,最直接的好处是省掉了传统SSH、SSM那一大堆XML配置。内嵌Tomcat,打包成jar就能跑,对新手极其友好。MyBatis和SpringBoot配合得也很成熟,mybatis-spring-boot-starter一引入,SQL自己写,复杂查询、联表统计都好控制。
为什么不用JPA?不是说JPA不行,而是这种管理系统的查询场景太杂了。房源列表要按区域、价格、户型、状态多条件组合查询,后台统计要按月份累加租金收入,这种活交给JPA反而别扭,要写复杂的Specification,还得担心有没有自动生成低效SQL。手写MyBatis的XML,每一条SQL都清清楚楚,出了问题也好排查。再加上国内很多教程、前人源码都是MyBatis体系,用它能少踩很多坑。
前端选Vue,核心原因是上手曲线平缓,生态完整。配合Element Plus做后台管理界面,表格、表单、弹窗这些现成组件往里套就行,不需要从零画UI。整套技术栈覆盖面广,既有后端业务逻辑,又有前端交互,还有数据库设计,对学习者和面试者来说都是性价比极高的组合。
MySQL作为数据库没什么好纠结的,免费、稳定、资料多。8.0以上版本安装方便,字符集记得用utf8mb4,后面基本不会因为中文出问题。
2. 数据库怎么设计:表结构、字段、关系一次讲清
2.1 核心表设计思路
这套系统的数据库设计,关系其实很清晰。我通常建议先看表结构,再看代码,因为表结构是业务的骨架。
主要的核心表大概有这些:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| sys_user | 用户表(管理员、房东、租客共用) | id, username, password, role, phone, nickname |
| house | 房源表 | id, user_id(房东), title, address, area, price, status |
| lease | 合同/租赁订单表 | id, house_id, tenant_id, landlord_id, start_date, end_date, status |
| bill | 账单表 | id, lease_id, bill_type, amount, status, pay_time |
| repair | 报修表 | id, house_id, tenant_id, content, status, handle_time |
| favorite | 收藏表 | id, user_id, house_id, create_time |
| notice | 公告表 | id, title, content, create_time |
这里面有几点设计经验值得展开说说。
第一,用户表没有拆成“管理员表、房东表、租客表”三张,而是用一张sys_user加role字段区分。这么做的好处是登录认证只需要查一张表,权限不同只影响菜单和接口校验,不会造成用户信息重复存储。当然,如果后期房东和租客的扩展属性非常多,再拆子表也来得及,初期完全没必要。
第二,lease表是整个业务的核心枢纽。它同时关联房源、租客、房东三方,这样设计可以避免出现“一个房源有多个未结束合同”这种脏数据。账单不直接挂在house上,而是挂在lease上,也很讲究。因为同一个房源租给不同租客会产生多份合同,每份合同都应该有自己独立的账单,如果账单只关联房源ID,钱就算不清了。
第三,状态字段统一用int或tinyint,比如0、1、2这种,不直接用字符串。数据库里存“已上架”三个字虽然直观,但后续做统计、做筛选,字符串的效率和写法都不如数字。代码里可以用常量类或枚举转义,前端再展示成中文,这是很常规的做法。
2.2 房源状态、合同状态、账单状态怎么流转
状态机是很多人读源码时容易绕晕的地方,这里我用表格直接列出来。
房源状态(house.status):
| 状态值 | 含义 | 下一步 |
|---|---|---|
| 0 | 待审核 | 管理员审核通过后变为1 |
| 1 | 已上架 | 可被租客申请;申请通过后变为2 |
| 2 | 已出租 | 合同到期或退租后回到1 |
| 3 | 已下架 | 房东或管理员下架,可重新上架为1 |
合同状态(lease.status):
| 状态值 | 含义 | 说明 |
|---|---|---|
| 0 | 申请中 | 租客提交申请,等待房东确认 |
| 1 | 待签约 | 房东确认,等待双方签约 |
| 2 | 履约中 | 合同生效,租客正常租住 |
| 3 | 已到期 | 合同时间自然结束 |
| 4 | 已退租 | 提前退租或到期后办结 |
账单状态(bill.status):
| 状态值 | 含义 | 说明 |
|---|---|---|
| 0 | 未缴纳 | 账单已生成,待租客支付 |
| 1 | 已缴纳 | 租客完成支付 |
| 2 | 已逾期 | 超过应缴时间仍未支付 |
| 3 | 已冲销 | 订单取消或合同终止后账单作废 |
实际源码里可能还有“审核状态”“支付方式”之类的字段,但核心状态就是上面这些。你只要抓住“房源状态靠合同驱动,账单状态靠支付驱动”这条规律,看代码里的状态判断就不会乱了。
2.3 关键查询SQL该怎么写
数据库部分最考验人的是多表联查和统计查询。比如后台首页要显示“本月平台房源总数、合同总数、租金收入总和”,如果只用mapper接口里写好的方法,往往会发现不够用,还是要自己写SQL。
一个典型的分页+条件查询,MyBatis的XML里大概是这样:
<select id="selectHousePage" resultType="com.demo.entity.HouseEntity"> SELECT h.*, u.nickname AS ownerName FROM house h LEFT JOIN sys_user u ON h.user_id = u.id <where> <if test="query.title != null and query.title != ''"> AND h.title LIKE CONCAT('%', #{query.title}, '%') </if> <if test="query.status != null"> AND h.status = #{query.status} </if> <if test="query.area != null and query.area != ''"> AND h.address LIKE CONCAT('%', #{query.area}, '%') </if> </where> ORDER BY h.create_time DESC LIMIT #{offset}, #{pageSize} </select>注意这里的写法是“逻辑外键”,也就是关联关系靠SQL运行时的JOIN来体现,而不是数据库物理外键。物理外键在删除、迁移、并发写入时容易产生各种约束问题,实际项目中绝大多数情况下都建议用逻辑外键,由代码层来保证一致性。
再看一个统计SQL场景,比如统计某套房子在某个时间段内的租金收入:
SELECT DATE_FORMAT(pay_time, '%Y-%m') AS month, SUM(amount) AS totalAmount FROM bill WHERE status = 1 AND lease_id IN ( SELECT id FROM lease WHERE house_id = #{houseId} ) GROUP BY month ORDER BY month ASC这种SQL在MyBatis里写起来非常顺手。数据库设计这块总结起来就是:表别拆太碎,状态用数字,关联靠逻辑外键,统计SQL不要怕写。把这几条想明白,你往后看任何管理类系统都会轻松很多。
3. 后端接口与代码结构:SpringBoot业务落地细节
3.1 分层、统一返回体与异常处理
看后端代码,先看包结构。主流版本基本都是这套分层:
- controller:接收请求、参数校验、返回结果
- service:业务逻辑,事务在这个层控制
- mapper:数据访问,对应MyBatis接口
- entity/model:数据库实体
- dto/vo:数据传输对象和视图对象
稍微好一点的源码,还会把DTO和VO分开。原因很简单:不要直接把数据库实体返回给前端。比如sys_user表里的password字段,如果接口直接把实体返回,等于把密码泄露了。正确做法是在controller层转成VO,把password字段去掉,或者只返回需要的字段。
统一返回体也很关键。推荐所有接口都返回同一个结构:
public class Result<T> { private Integer code; private String msg; private T data; }code为200表示成功,其他值表示各种错误。这样前端axios拦截器只要判断code就能统一处理,比如401跳登录页,403提示没有权限,500弹错误提示。
异常处理我建议用@RestControllerAdvice配合@ExceptionHandler,别在每个controller里写try-catch。业务层抛自定义异常,全局处理器统一捕获,代码会干净很多。
3.2 权限是怎么控制的
这个项目没引入Spring Security,而是用“拦截器 + Token”的方式,这其实很聪明,也符合这类型项目的体量。
流程是这样:
- 用户登录成功,后端生成一个token返回给前端。
- 前端把token存到localStorage,每次请求放在请求头里。
- 后端写一个拦截器,配置到WebMvcConfigurer里,拦截除登录、注册之外的所有接口。
- 拦截器解析token,从redis或数据库里查用户信息,放行或拒绝。
如果要细分角色权限,可以在自定义注解上加角色标识,比如@RequireRole("ADMIN"),没有对应角色就直接拦下。这样比引入一套完整安全框架要轻量,也更容易在答辩的时候讲清楚原理。
这里有一个很容易踩的坑:不要只在前端菜单层面对角色做隐藏,后端接口也必须做角色校验。因为懂技术的人完全可以绕过前端直接调接口,如果不校验角色,拿一个租客账号就能把管理员接口翻个底朝天,这是严重越权漏洞。
3.3 租房签约流程的状态流转
租房签约是整个后端代码里比较核心的一段业务,值得单独拿出来说。
租客提交申请时,前端会传房源ID、租期、租金这些数据。后端要做的事情大概有三步:
- 查房源状态,确认房源存在且状态为“已上架”,不是已出租、已下架。
- 插入一条lease记录,状态为“申请中”。
- 注意防重:同一个租客不能对同一套房源反复提交申请。可以加一个唯一约束,也可以查询当前是否有未结束的申请。
房东确认申请的逻辑,是把lease状态从“申请中”改成“待签约”,然后再确认签约时,状态变成“履约中”,同时把house状态改成“已出租”。
这里面有个业务细节要注意:修改房源状态不能盲目UPDATE,应该带上条件防止覆盖。
@Transactional(rollbackFor = Exception.class) public Result<?> createLease(LeaseDTO dto) { House house = houseMapper.selectById(dto.getHouseId()); if (house == null || house.getStatus() != HouseStatus.ON_SALE) { return Result.error("房源不存在或不可租"); } Lease lease = new Lease(); // 填充房源、租客、租期字段 lease.setHouseId(house.getId()); lease.setTenantId(dto.getTenantId()); lease.setLandlordId(house.getUserId()); lease.setStatus(LeaseStatus.APPLYING); leaseMapper.insert(lease); // 如果是直接签约流程,房态置为已出租 houseMapper.updateStatus(house.getId(), HouseStatus.RENTED); // 批量生成账单 billService.generateByLease(lease); return Result.success(); }看到这里你应该能明白,为什么数据表之间要有清晰的状态关联。如果合同状态和房源状态各自乱变,整个系统很快就会产生“房子显示已租但合同还是申请中”这种脏数据。
3.4 文件上传和图片访问问题
房源信息一般都要上传图片,源码里常见做法是用MultipartFile接收文件,保存到服务器本地某个目录,然后把访问路径存进数据库。
这里必须配置一个虚拟路径映射,否则保存成功之后,前端访问图片URL会404。SpringBoot中可以在配置类里这样写:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath + "/"); } }图片存储这块有几个真实教训:上传路径不要写死死盘的绝对路径,尽量用配置项;文件名用UUID重命名,避免中文名和重名;限制文件大小,免得传大图把服务器塞满。这些都是小问题,但现场跑项目的时候碰到了,真能卡你一下午。
4. 前端页面组织与核心交互实现
4.1 前端工程结构、路由和状态管理
这版前端是Vue3 + Vite + Element Plus + Pinia的结构。相比Vue2的Options API,Vue3的组合式API写起来确实更利索,但代码风格上也更考验组织能力。
目录结构大概长这样:
src/ |-- api/ # 按模块封装的请求函数 |-- assets/ # 静态资源 |-- components/ # 公共组件 |-- router/ # 路由配置 |-- store/ # Pinia状态管理 |-- utils/ # 请求封装、公共方法 |-- views/ |-- admin/ # 管理员页面 |-- landlord/ # 房东页面 |-- tenant/ # 租客页面 |-- login.vue |-- layout.vue路由权限这里有个比较实用的做法:前端根据登录用户的角色动态生成可访问的路由表,而不是把所有页面都写死在路由里。具体来说,router里只保留公共路由,登录后根据角色从后端拿菜单权限,再用router.addRoute动态注册。这样租客的浏览器里根本不存在管理员的页面路径,体验上更安全,也更好解释。
axios请求封装需要统一处理token和错误码:
service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 401) { router.push('/login') } return res }, error => { return Promise.reject(error) } )这段是基础中的基础,但很多人都栽过。比如后端返回的code不是200,前端却只判断HTTP状态码,就会导致接口报错但页面没有任何提示,难排查得很。
4.2 房源列表、详情页与租赁申请
房源列表页是租客家体验的核心页面。一般会做搜索栏,支持关键字搜索、区域筛选、价格区间、状态筛选,底部还有分页。这里有个交互细节:每次切换筛选条件,页码要重置回第1页,不然会出现“搜索后在第三页显示”这种尴尬。
列表接口分页参数我用pageNum和pageSize,后端返回总条数total,前端用total控制分页组件。页面请求写法不复杂:
export function getHousePage(params) { return request({ url: '/house/page', method: 'get', params }) }详情页的重点是展示房源信息、房东信息、状态标签,以及“申请租房”按钮。点击申请后,弹出一个表单,选起始日期、租期,提交到后端。申请成功后按钮禁用,提示等待房东确认。这种状态处理的体验很重要,不然用户会以为按钮坏了,反复点击还容易产生重复申请。
收藏功能可以做得很轻量,点击图标后调addFavorite或deleteFavorite接口,前端用本地状态临时切换样式,不刷新页面。不过要注意,如果接口失败要做回滚,不然显示收藏成功了,刷新后其实没有。
4.3 管理后台的统计图表是怎么来的
管理员首页通常会有几个ECharts图表,比如近半年租赁合同数量走势、房源类型分布、租金收入月度曲线。这些图表数据不是前端自己算出来的,是后端直接聚合好返回的。
后端提供接口,返回类似这种结构:
[ { "month": "2025-01", "count": 12 }, { "month": "2025-02", "count": 18 }, { "month": "2025-03", "count": 22 } ]前端拿到数据,只是把它填充到ECharts的series里,避免在前端做复杂聚合。这也是前后端分工的典型做法:能后端算的不要推到前端,既减少前端性能消耗,也方便口径统一。统计SQL就是前面提到的GROUP BY相关写法。
如果再往后做,还可以用Redis缓存这些统计结果,因为首页统计其实不需要每次请求都打数据库,缓存一分钟完全够。这个优化点放在项目总结里写出来,会是很不错的加分项。
5. 从零跑起来:环境准备、踩坑记录与演示建议
5.1 本地启动的完整步骤
不管你是从哪个渠道拿到的源码,启动步骤基本都一样。按这个顺序来,可以减少很多不必要的折腾。
第一步,准备环境。JDK版本要注意,SpringBoot 2.7.x配JDK8或11都没问题,如果是SpringBoot 3.x就必须JDK17以上。MySQL至少用5.7,推荐8.0,安装的时候字符集选utf8mb4。前端要装Node.js,Vite 4/5对Node版本有要求,建议直接用Node 18+。
第二步,创建数据库并初始化数据。在MySQL里新建数据库,然后执行源码里的sql脚本,一般放在项目的sql目录下。执行完能看到sys_user、house这些表,还会自带几个账号,比如admin管理员、房东号、租客号。
第三步,改后端配置。打开application.yml,把数据库地址、用户名、密码改成自己的:
spring: datasource: url: jdbc:mysql://localhost:3306/house_rent?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 servlet: multipart: max-file-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true这里有几个参数是必须保留的:serverTimezone要设成Asia/Shanghai,不然数据库连接会报时区错误;allowPublicKeyRetrieval和useSSL在MySQL 8.0下经常会需要,不然启动报错能让你怀疑人生。
第四步,启动后端。用IDEA打开后端工程,等Maven把依赖都下载完,然后启动主类。启动后控制台出现Tomcat started on port 8080就说明成功了。
第五步,跑前端。在终端进入前端目录,执行npm install,如果下载慢可以换国内镜像源。装完后执行npm run dev,Vite启动后会给一个本地地址,浏览器打开,用初始化账号登录。
5.2 高频问题排查速查表
这一块是我从实际帮人调试过程中整理出来的,基本覆盖了90%的启动问题。
| 问题 | 原因 | 解决办法 |
|---|---|---|
| 后端启动失败,提示端口被占用 | 8080端口被其他进程占用 | 换端口或关掉占用进程,配置server.port |
| 数据库连接失败 | 密码错误、时区、SSL参数 | 核对application.yml,补allowPublicKeyRetrieval和serverTimezone |
| 前端接口全部404 | 后端接口前缀没对上 | 检查前端BASE_URL是否以/api开头,后端context-path是否一致 |
| npm install报错 | Node版本不兼容 | 切换Node版本,Vite5建议18+ |
| 页面白屏,控制台报JS错误 | 依赖版本冲突或路由懒加载问题 | 删除node_modules和package-lock.json后重装 |
| Element Plus图标不显示 | 图标组件未单独注册 | 按需引入或全量引入图标组件 |
| 上传图片访问不到 | 虚拟路径没有配置 | 检查addResourceHandlers |
| 中文乱码 | 字符集不一致 | MySQL库、表、连接串全部用utf8mb4 |
里面很多坑都是环境导致的,和代码本身关系不大。遇到问题别急着怀疑源码,先看日志,日志里的异常信息才是最靠谱的线索。
5.3 演示Demo时几个加分细节
如果你要拿这个项目做课程设计或者面试项目演示,不建议照着代码念。提前准备一套演示脚本,效果会好很多。
我的建议是准备三套房源数据:一套已上架待租,一套已出租,一套已下架,图片换成真实感强一点的。演示流程按真实业务来走:先用管理员账号登录,展示用户列表、房源审核、统计数据;然后切换房东账号,发布一套房源并让管理员审核通过;再切换租客账号,搜索房源、发起申请;接着回到房东账号确认合同;最后用租客账号查看待缴账单,模拟缴费。这样一整套走下来,业务闭环完整,比单纯点菜单有说服力得多。
演示之前把浏览器缓存清掉,数据库重置成初始状态,避免上次测试留下的脏数据造成尴尬。现场不要现场改代码,哪怕只是改一个console.log,都可能因为编译时间卡壳。
6. 二次开发与扩展方向:别让项目止步于跑通
6.1 低成本、见效快的5个改进点
系统跑通只是第一步,想让它变成真正拿得出手的作品,下面这些改进都很值得做。
第一,引入Redis缓存。热门房源列表和首页统计数据是典型的读多写少,缓存起来能明显提升性能,写出来也是亮点。
第二,把权限从拦截器升级成Spring Security + JWT。虽然现有拦截器够用,但如果你在简历里写Spring Security,面试官很可能追问,能提前掌握会更有优势。
第三,增加站内信或邮件通知。租客申请租房后,自动给房东发通知;账单生成时,给租客发提醒。用Spring事件机制来解耦,不会侵入现有业务代码。
第四,接入模拟支付。第三方支付网关申请麻烦,但做一个“模拟支付页面”来演示流程没问题,重点是梳理清楚支付回调、更新账单状态、确认到账这个链路。
第五,用Docker Compose做一键部署。把MySQL、后端jar、前端静态文件打成容器,本地写一个docker-compose.yml,这样项目部署能力直接上一个台阶,而且很适合写进部署文档。
6.2 换技术栈时要注意的迁移问题
有人会想把MyBatis换成MyBatis-Plus,或者把前端从Vue3换成Vue2,这都可以,但有几个坑提前知道能少走弯路。
MyBatis换MyBatis-Plus之后,大部分单表CRUD可以直接用Wrapper,但原来的XML分页查询如果要继续用,分页插件要单独配置。而且PageHelper和MyBatis-Plus的分页拦截器二选一,同时引入会冲突,这是一个非常经典的报错点。
前端如果从Vue3切回Vue2,需要把组合式API改成选项式API,Element Plus也要换成Element UI,组件名称和用法都有差异。这活儿其实不小,所以我一般建议别轻易改框架,如果一定要改,就提前给自己留一整天时间。
6.3 源码维护的几条实在建议
最后聊点维护层面的经验。第一,数据库脚本要跟上业务改动,每加一个字段就更新sql文件,别让新同事跑旧脚本上线。第二,上传图片的目录不要塞进代码仓库,配置好.ignore,否则项目包会越来越大。第三,每个接口最好都写个简单的文档,不用很formal,列出路径、参数、返回结构就行,后面查问题能救命。
我一直觉得,源码最好的使用方式不是照抄,而是读懂它、改过它、踩过它的坑,再把它变成自己的东西。这个项目虽然不复杂,但完整业务链路一旦走通,你对SpringBoot、MyBatis、Vue的认知会提升一大截。如果你正准备做类似的管理系统,希望这篇拆解能让你少走几段弯路。