☰
SpringBoot3+Vue3房屋租赁管理系统全栈实战
2026/10/10 16:49:00 网站建设 项目流程

1. 为什么我会重新做一套房屋租赁管理系统

先说项目背景。去年有几位做毕设的朋友陆续找我,说想选"房屋租赁管理系统"这个题目,问我有没有参考项目。我看了下市面上流传的源码,多数停留在"能跑通登录+简单CRUD"的层面:权限靠前端按钮隐藏、合同状态靠手改数据库、账单和租期完全没有联动。更典型的问题是房源一被租出去,仍然能继续下单预约,业务上根本是断的。

索性我自己重新梳理了一版,把真实租房业务里该有的闭环做完整。这套系统的核心链路是这样的:房东登录后发布房源,房源审核上架,租客浏览并预约看房,房东确认带看,双方谈妥后在线签署合同,系统按合同周期自动生成房租账单,租客缴纳后留痕,合同到期或退租后房源重新释放。除了这些主线功能,还配套了用户管理、公告反馈、数据看板等辅助模块。

技术栈是标题里写的这套:SpringBoot 3 + Vue 3 + MyBatis-Plus + MySQL 8。如果你正准备做毕业设计、课程设计,或者刚学完前后端分离想找一个完整项目练手,这套源码的代码结构、数据库设计、权限控制和部署方式,都是有实际参考价值的。我会在后面把选型理由、表结构思路、核心业务的状态流转逻辑,以及我在开发过程中踩过的坑全部摊开讲。

这个项目最让我意外的一点是,看起来平淡无奇的CRUD,真正做进业务细节后居然这么折腾。但也正因为折腾,它才值得写这篇文章。

2. 技术选型的取舍:不追新,但也不守旧

2.1 后端为什么锁定SpringBoot 3.x + JDK 17

2025年再开新项目,如果还选SpringBoot 2.7.x,我是不太理解的。2.x版本在2023年底就停止了商业支持,社区新特性也都往3.x靠。SpringBoot 3.x强制要求JDK 17起步,这对很多习惯JDK 8的老开发是个心理门槛,但从实际部署看,JDK 17的默认GC参数对小型应用的内存占用更友好,启动速度也更快。

我在这套系统里实际用到的特性其实不算多,但3.x带来的好处是明显的:jakarta命名空间统一了,javax那套老API彻底退役;Spring Doc生成接口文档时和OpenAPI 3的兼容性更好;未来如果想接虚拟线程,SpringBoot 3.2以上版本直接改一行配置就能体验,2.x想都不用想。

需要注意一个低级但高频的坑:如果你现在的开发机器装的还是JDK 8,直接跑SpringBoot 3会直接报UnsupportedClassVersionError,根本没有商量的余地。所以选这套技术栈的前提,是先把JDK版本升到17或21。

2.2 前端为什么是Vue 3 + Vite + Element Plus + Pinia

前端我没有选Vue 2,原因很现实:Vue 2官方已经停止维护了。Vue 3配合Vite之后,开发体验完全是两个时代,冷启动从原来的十几秒变成秒开,热更新也不会每次改几行代码就卡顿。

组件库用的Element Plus,后台管理类页面它依然是最成熟的选择。状态管理用Pinia不用Vuex,因为Pinia的store写法更接近组合式API的自然表达,没有那么多概念,新人上手快。路由用Vue Router 4,配合前置守卫做登录校验和角色菜单过滤,这个我在权限控制部分会细讲。

一个容易被忽略的点:构建工具从Webpack切到Vite后,很多老教程里的写法已经不适用了。比如环境变量从process.env.VUE_APP_XXX变成了import.meta.env.VITE_XXX,代理配置从vue.config.js挪到了vite.config.js里。这些问题在复制别人代码时特别容易踩,后面我会专门说。

2.3 ORM选型的纠结:MyBatis-Plus是务实之选

这个项目我用的是MyBatis-Plus,而不是原生MyBatis或者Spring Data JPA。做一个直白的对比:

方案适合场景主要问题
原生MyBatis复杂SQL极多、DBA把控严格的团队通用CRUD也要手写XML,代码量巨大
Spring Data JPA模型驱动、关联关系复杂的业务多条件动态查询不直观,SQL难以精细控制
MyBatis-Plus中小业务系统,CRUD占大头超复杂SQL仍需手写XML兜底

房屋租赁系统的查询多数是"按条件筛选列表"——比如按价格、区域、户型筛房源,按状态、类型筛合同和账单。MyBatis-Plus的LambdaQueryWrapper在这种场景下几乎是最舒服的写法。更重要的是它的分页插件PaginationInnerInterceptor,一行配置就能拿到total和pages,比手写拦截器容易一个数量级。

数据库用MySQL 8.x,不再用5.7。一方面MySQL 5.7已经EOL,更重要的是8.0默认字符集就是utf8mb4,中文和emoji都不会出乱码;统计看板里需要的窗口函数,在8.0里是原生支持的。

2.4 权限方案没有用Spring Security,而是JWT + 拦截器

这点我估计会被一些人质疑。Spring Security是很标准,但它这层过滤链对中小项目来说有点重,而且配置出错时排查难度很大。这个系统角色只有三种:管理员、房东、租客,权限控制没有细到按钮级。所以我选择用JWT + HandlerInterceptor + 自定义注解,逻辑清楚,出问题也容易定位。

实际设计是登录成功后签发token,前端每次请求放在Authorization头里;后端拦截器解析token,把当前用户信息放进ThreadLocal,Service层需要当前用户时直接从上下文取。接口上标注@RequireRole注解,指定访问角色列表,拦截器里做匹配校验。

这套方案的短板是token无法主动失效,真要遇到账号被禁用,也只能等token自然过期。我的处理是把token有效期设为24小时,加上一个简单的在线用户管理接口,管理员禁用用户后,该用户下一次请求时后端还会查一次用户状态,状态为0会直接拒绝。等于用一次数据库查询换来了实时的封号能力,代价很小,效果够用。

3. 数据库建模:先想清楚业务约束,再动表结构

3.1 功能模块的全景划分

我把系统拆成七个模块:

  1. 用户管理:管理员维护用户列表,支持启停;房东和租客自助注册或由管理员创建
  2. 房源管理:房东发布房源、上传图片、上下架管理;租客端做多条件搜索和详情查看
  3. 预约看房:租客发起预约,房东确认或拒绝,带看完成标记
  4. 合同管理:合同生成、确认签署、变更状态、附件上传下载
  5. 账单管理:房租/水费/电费/维修费账单生成、缴纳、逾期标记
  6. 通知公告:管理员发布公告,租客、房东在首页查看
  7. 数据看板:房源数、签约数、在租数、应收实收金额等统计图表

模块之间不是割裂的。房源和合同强关联,合同和账单强关联,账单催缴又反向影响退租流程。建模时一定要把这些关系的约束想清楚。

3.2 核心表的字段设计与建表原则

我建了八张核心表:用户表、房源表、房源图片表、预约表、合同表、账单表、公告表、反馈表。挑几个有代表性的说一下设计思路。

用户表里有个关键字段role,用int存(1管理员,2房东,3租客),密码用BCrypt加密后的字符串存储,而不是明文。username加唯一索引。status字段做启停控制。

房源表的核心是landlord_id外键关联用户表、price字段用decimal(10,2),status用int表示流程状态。房源描述不能图省事用varchar(255),要直接设成text类型,否则房东写长一点就报错。

合同表需要特别注意。合同编号contract_no用业务规则生成,格式类似HT20250101001,即HT+日期+当日序号,唯一索引。租期字段用start_date和end_date两个date类型,押金和月租都是decimal(10,2)。status作为整个系统最关键的状态字段,取值范围我后面单独讲。

账单表的重点是加了一个bill_month字段来记录账单所属月份,和contract_id、bill_type一起建立唯一索引,从机制上防止定时任务重复生成账单。没有这个约束,写定时任务的人早晚会被自己的逻辑背刺。

3.3 为什么不做物理外键和枚举表

我的表全部采用逻辑关联,没有在数据库层面加外键约束。这个决策在团队协作里可能会被质疑,但在这个场景下是有道理的:业务高并发写入时,物理外键会带来额外的锁开销;一旦将来要分库分表,物理外键更是迁移的噩梦。一致性问题交给Service层在事务里控制,配合必要的唯一索引,完全够用。

状态字段也没有单独建枚举表,直接用一个int常量类统一管理。比如合同状态下面的常量:

public class ContractStatus { public static final int WAIT_SIGN = 0; // 待签署 public static final int ACTIVE = 1; // 履约中 public static final int EXPIRED = 2; // 已到期 public static final int CLOSED = 3; // 已退租 public static final int CANCELED = 4; // 已取消 }

前端拿到int值后通过字典翻译成中文显示。这样做的好处是数据库查询效率高、代码里写判断时语义清晰,缺点是写SQL统计时必须记熟每个数字的含义。我在实际项目里见过有人用varchar存状态描述,查询时每次都要写前面模糊,简直是噩梦,不推荐。

4. 核心业务的硬骨头:租约状态流转如何做到不失控

4.1 合同状态机的定义与流转规则

整个系统里最容易写乱的就是合同状态。我在一开始就定义了一张明确的状态流转表,绝不允许Service层出现随意的状态赋值。

当前状态允许的下一状态触发动作
待签署(0)履约中(1)、已取消(4)租客确认签署、任意一方取消
履约中(1)已到期(2)、已退租(3)定时任务扫描到期、租客办理退租
已到期(2)已退租(3)退租申请并结清账单
已退租(3)无终态
已取消(4)无终态

实现方式没上状态机框架,而是在Service层封装一个状态校验工具类。每个变更状态的方法进来第一步就是检查当前状态是否在允许的转移列表中,非法操作直接抛业务异常,提示语就是"当前合同状态不允许该操作"。这看起来简单,但它能挡住后续接手的同学胡写。状态量如果超过十个,我才会考虑引入真正的状态机框架,这个项目的体量没必要。

4.2 合同签署与履约中的并发控制

合同的签署动作要处理并发问题。设想一个场景:房东和租客A谈好了,合同生成待签署,此时租客B恶意重放接口也试图签署这单合同。如果不做控制,可能出现双方都签署成功,状态被覆盖。

我的做法是在合同表加一个version字段,所有状态更新操作走乐观锁。更新SQL里带上version = ?条件:

UPDATE contract SET status = #{targetStatus}, version = version + 1 WHERE id = #{id} AND version = #{oldVersion}

受影响行数为0说明版本已被其他事务修改,直接提示"数据冲突,请刷新后重试"。在中小型系统里,乐观锁比数据库行锁实现起来更简单,也不会因为长事务把连接池拖垮。

签约成功之后还要顺带处理房源状态,将房源从"已上架"变为"已租出",并且这个操作必须在同一个事务里完成。事务里先更新合同状态,再更新房源状态,最后生成第一张房租账单,任何一个环节失败都整体回滚。我在实际编码时用了Spring的@Transactional(rollbackFor = Exception.class),注意rollbackFor很重要,否则捕获了RuntimeException之外的自定义异常,事务不会回滚,这是很多新人容易踩的坑。

4.3 账单生成的防重设计与定时任务

账单这块,如果只做"生成账单"这个动作,第一次写可能觉得很直白,但一旦涉及定时任务,重复生成的问题就来了。我的方案是定时任务每天晚上扫描所有履约中的合同,按月份尝试生成账单。

关键的防重不是靠任务里的标记位,而是靠数据库唯一索引。我在账单表上建了uk_contract_month_type(contract_id, bill_month, bill_type)唯一索引。插入时就算任务由于某种原因跑了两遍,第二遍也会因为唯一索引冲突而失败,然后被吞掉;而不会产生重复账单。

定时任务用的是Spring自带的@Scheduled注解,配合一个开启开关的配置项。生产环境下可以部署独立的任务节点,但当前体量单服务内没问题,只要设置合理的cron表达式,并且把任务执行日志打出来方便追溯。

顺带说一个细节:每月房租账单的到期日我设置为当月的最后一天,计算逻辑用YearMonth和LocalDate.with(TemporalAdjusters.lastDayOfMonth())。千万不要自己拼接字符串去算某月的最后一天,二月份和闰年分分钟让你怀疑人生。

4.4 预约看房的冲突检测

预约看房表面上是简单的insert,实际暗含一个约束:同一房源不能在同一时间段有多个有效预约。我加了一个校验逻辑,在插入预约记录前先查询该房源下是否存在状态为待处理或已确认、并且时间区间有重叠的有效预约。

如果只想做最简单的防重,可以利用数据库层对appointment表增加house_id和appointment_time的唯一约束。但如果允许时间段而不是时间点,唯一约束就不够用了,需要走应用层查询判断。

考虑到并发窗口,我在房源表上加了一个version乐观锁,预约创建成功后对房源状态做一次版本递增。这样一来,即使两个请求同时提交,也只有一个会成功。

5. 开发过程中踩过的坑:每一条都是真金白银换来的

5.1 MySQL 8时区导致的时间差八小时

第一个让我印象深刻的坑,来自MySQL 8默认时区是UTC。我本地把系统时间写进数据库,查出来差8小时。排查问题时,发现show variables like 'time_zone'返回的是UTC,而Java侧的LocalDateTime没有任何时区概念。

解决方式是在JDBC连接串上明确指定:

jdbc:mysql://127.0.0.1:3306/rental_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

同时Java代码里的时间类型统一用LocalDateTime,不要混用java.util.Date。实体类里也不要图方便用字符串存时间,否则排序、区间查询都会很麻烦。

5.2 MyBatis-Plus自动填充总是填充不进去

我建表时有create_time、update_time,希望在插入和更新时自动填充,结果写完MetaObjectHandler后字段还是空。排查后发现实体类上少了@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE)。

有了注解之后,MetaObjectHandler里还要注意使用this.strictInsertFill而不是setFieldValByName。后者的坑在于如果你在实体类里手动给某些字段设置了值,填充逻辑可能把它覆盖掉。strictInsertFill只会在字段为空或默认值时填充,行为安全得多。

还有一个很容易忽略的映射问题:实体类是createTime,数据库列是create_time。虽然MyBatis-Plus默认开了驼峰映射,但如果你手写的XML里自定义了resultMap,又没有把映射字段补全,这里照样会空。排查方向不要只盯在处理器上。

5.3 前端Vue Router history模式刷新404

开发环境下一切正常,打包上线后用户刷新页面突然变成404。这是非常典型的Vue路由history模式问题。createWebHistory模式下,浏览器刷新会向服务器请求/login、/contracts/list这样的真实路径,服务器没有对应的静态文件,自然404。

解决方式在Nginx里配上try_files:

location / { root /var/www/rental-web/dist; try_files $uri $uri/ /index.html; }

这样Nginx遇到不存在的路径会回退到index.html,前端路由接管后正常渲染。这个配置一定要放在location /里,而不是只放在某个子路径下。

5.4 自定义多表关联SQL的分页数据不对

后期做数据看板时,我需要关联合同表和账单表做统计,直接用MyBatis-Plus的selectPage传入自定义Mapper方法,结果发现total统计完全错误,翻页后的数据也不对。

原因是看板SQL里存在一对多的join,比如一个合同对应多条账单,count(*)统计的物理行数大于真正合同的条数。解决办法是在查询中先去重关联,或使用子查询统计合同表本身的数量,而不是join后的结果行数。

更标准一点的思路:如果只是想获取关联数据的分页,可以把主表分页条件封装到IPage参数里,让数据库做分页后再关联子表。这个顺序很重要,先分页后关联,而不是先关联后分页。

5.5 文件上传的默认大小限制

房源图片和合同附件都要走文件上传接口。本地测试时传一张几兆的图片没有感觉,换了个更大的文件就报MaxUploadSizeExceededException。SpringBoot默认单文件上传上限只有1MB。

在配置文件里需要手动调大:

spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB

同时注意上传时目录不要写死绝对路径。我的做法是在application配置里注入一个自定义上传目录,生产环境用环境变量覆盖,避免把本地路径带到服务器上,也方便后续把文件迁移到对象存储。

5.6 开发环境的跨域为什么不要开CorsFilter

前后端分离开发,最省事的做法是后端配一个CorsFilter允许所有来源。但一个项目如果从开始就一直全放开跨域,上线后Nginx同域配置时很容易产生一些"之前好好的现在都不行了"的错觉。

我建议开发环境用Vite的proxy做代理转发,前端访问/api,Vite把请求代理到后端localhost:8080。生产环境用Nginx配置统一域名和API反向代理。整个生命周期里根本不需要跨域配置,Code里也干净,不需要为CORS写一堆允许列表。

6. 项目落地:从打包到上线,我用的这一条完整链路

6.1 多环境配置与环境变量管理

项目里我用Spring的profile机制分成三个环境:

  • application.yml:公共配置,放端口、通用配置
  • application-dev.yml:本地开发数据库、日志级别
  • application-prod.yml:生产数据库、线上日志策略

生产数据库的密码、密钥这几个敏感字段绝不写死在文件里提交到Git。我利用Spring的环境变量占位符,写成${DB_PASSWORD}这种形式,在服务器上通过export DB_PASSWORD=xxx注入。这样即便仓库代码泄露,也不会把数据库密码带出去。

6.2 打包与静态资源发布

后端就是一个标准SpringBoot jar包:

mvn clean package -DskipTests

打包后通过nohup java -jar方式启动。JVM参数可以参考这个水平,小型项目不需要太豪华:

nohup java -Xms512m -Xmx512m -XX:+UseG1GC -jar rental-system.jar > app.log 2>&1 &

前端构建:

npm run build

产物dist目录上传到服务器,Nginx的root指向它。完整Nginx配置如下:

server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /var/www/rental-web/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件的静态访问 location /uploads/ { alias /data/rental-system/uploads/; } }

6.3 数据库初始化和备份脚本

数据库脚本在项目docs目录下,上线执行时注意先确认字符集:

CREATE DATABASE rental_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

上线后最不能省的是备份。写个简单的crontab脚本,每天凌晨执行mysqldump,保留最近7天:

0 2 * * * mysqldump -u root -p$DB_PASSWORD rental_system > /backup/rental_$(date +\%Y\%m\%d).sql && find /backup -type f -mtime +7 -delete

别笑,很多小项目死在"数据库没备份"这个低级失误上。租务数据丢了补不回来的,用户交租记录不是开玩笑的。

6.4 上线初期要重点盯的几个监控指标

上线后不要只看页面能不能打开。我建议至少盯三样东西:

一是应用日志里的异常堆栈,重点看数据库连接池是否打满、慢SQL是否拖垮接口。可以在MySQL里开启慢查询日志,设置long_query_time=2,定期筛出来优化。

二是磁盘空间,服务器磁盘被日志撑满这种事故太常见了。logback里做按天滚动和保留天数,上传目录定期清理垃圾文件。

三是定时任务是否正常执行。账单生成任务如果某天挂了,一个月后才被发现,那麻烦就大了。我通常在任务方法里写结果日志,再用一个简单的健康检查接口把最近一次任务执行时间暴露出来,方便监控。

7. 最后再分享一点我个人的实操体会

这个项目做完之后,我最大的感触是"租赁管理系统的核心不在CRUD,而在约束"。房源能不能被重复预约、合同状态能不能被非法跳转、账单会不会重复生成,这些才是真正决定项目质量的点。如果只会搭SSM框架然后堆接口,做出来的系统是没法拿到真实场景里用的。

还有一个经常被低估的功能:账单提醒。我在实际跑测试数据时发现,很多租客并不会每天登录系统看账单,到期不缴的情况比想象中多。后来加了一个简单的公告栏提醒和历史缴费记录展示,情况好很多。做类似系统的时候,不要只盯着功能数量,把"催缴-记录-对账"这个小闭环做扎实,比多加五个花哨页面有用得多。

最后一个小技巧给准备拿这套系统做毕设的同学:数据库初始化脚本里我放了一些模拟数据,包括几套房源、几份不同状态的合同、一批横跨多月的历史账单。演示的时候这些数据特别有说服力,比现场裸建一套干净数据强得多。我当年做答辩项目时就是靠这批真实感极强的模拟数据,把"退租未结清账单不能办理"这个业务约束讲得明明白白。

项目到这里就算完整收尾了。源码结构、表结构、部署脚本都整理在项目目录里,照着文档一步步搭建,遇到问题也可以拿这篇文章对照排查。希望这套系统能帮你少走一点我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询