1. 为什么毕业设计选"房屋租赁系统"而不是老套的"图书管理系统"
每年到这个时间点,私信里问得最多的就是"毕设选什么题"。图书管理系统、学生选课系统、实验室预约系统,这些题目十年前是经典,现在已经是答辩老师眼里的同质化重灾区。如果你想选一个功能不复杂、又能讲出技术点的题目,基于 Java + Vue 的小型房屋租赁系统,确实值得认真考虑。这套项目覆盖管理员、房东、租客三类角色,包含房源上下架、预约看房、签约、账单生成、退租结算的完整业务链路,配套源码、数据库脚本和说明文档,完全可以作为毕业设计核心项目来复现和改造。
为什么我推荐这个题目?理由很实在。
第一,它业务场景真实。租赁市场里有真实的角色、真实的规则、真实的钱款计算。"一套房子从哪里来,到哪个租客手里,每个月产生多少租金,退租时怎么结算",这套逻辑不是凭空捏造的,而是能讲给外行听的。毕业设计答辩时最怕的是"老师根本看不懂你在做什么",租赁场景不存在这个问题,反而能让老师快速理解系统价值。
第二,它有天然的技术难点。纯增删改查很难拿高分,但租赁系统里天然有几个值得展开的点:房屋状态机(空置→预订→已租→维修→退租)、合同计费规则(月付、季付、押几付几)、账单生成时机、一房多租的并发防护。哪怕只把状态机这一块做透,答辩时也能有理有据地讲上五分钟,这比堆砌十个"看起来高大上但答不上来"的功能有用得多。
第三,它适配当前主流技术栈。Spring Boot + Vue 是近几年就业市场认可度很高的组合,前后端分离的开发模式也和企业实际项目一致。做完这套项目之后,简历上写"独立开发一套前后端分离的房屋租赁管理系统",说服力远强于"图书管理系统"。
适合哪些人参考?如果你 Java 基础薄弱,只写过一点 Servlet 和 JSP,这个项目是很好的进阶跳板;如果你已经有一定基础,想用完整项目把前端、后端、数据库串起来,它同样是合适的练手素材。关键点是:系统跑起来之后,你能不能把每一个表为什么这样建、每一个接口为什么这样写讲清楚。能讲清楚,分数就不会低。
下面我把这套系统从技术选型、数据库设计、核心模块、部署避坑、答辩准备几个方面拆开讲,给你一些可以直接上手操作的思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型:Java + Vue 的前后端分离是怎么定的
2.1 后端框架:为什么不用 SSM 而用 Spring Boot
很多老教程还停留在 SSM(Spring + SpringMVC + MyBatis),更老的还有 SSH(Spring + Struts + Hibernate)。这些框架的 XML 配置过程十年前是必考项,今天做毕业设计再走一遍,纯属给自己挖坑。Spring Boot 把绝大多数配置自动化,内置 Tomcat,打一个 jar 包就能跑,开发体验完全不一样。你真正要做的是把业务逻辑做扎实,而不是证明自己会写 XML。
版本选择上,我建议用 Spring Boot 2.7.x 搭配 JDK 8 或 11。这个组合的资料密度最高,遇到任何报错基本都能搜到现成解决方案。Spring Boot 3.x 虽然更"新",但要求 JDK 17 以上,部分第三方兼容性还没完全跟上,毕设阶段别为了追新给自己增加排错工作量。
2.2 前端框架:Vue 2 还是 Vue 3
Vue 在主流前端框架里属于"新手最容易上手"的那一类。做毕设时我一般建议:参考项目用什么版本,你就沿用什么版本。如果参考项目是 Vue 2 + Element UI,就老老实实继续用;如果是 Vue 3 + Element Plus,也保持一致。最怕的是中途产生"我觉得版本太老"的想法,自己动手升级一版,组件库 API、全局挂载方式、路由写法都有差异,升级过程中冒出来的兼容问题足够折腾一周。
如果完全没有参考、从零开始选,我的倾向是 Vue 2 + Element UI。原因只有一个:教程量和踩坑记录足够多,遇到问题一搜就能找到答案。毕设的评分点在于功能完整度、逻辑清晰度和答辩表现,"用了最新版本"本身不加分,但"版本问题导致项目跑不起来"一定会扣分。
2.3 登录鉴权:为什么是 JWT 而不是 Session
前后端分离的项目里,用 Session 方案意味着要处理 Cookie 跨域、后端状态存储等问题,配置和理解成本都不低。JWT 方案则是无状态的:用户登录成功后,后端把用户 ID、用户名、角色等信息签名到一个 Token 字符串里返回前端,前端存在 localStorage,每次请求在请求头带上这个 Token。后端接口通过拦截器校验 Token 的签名和有效期,不需要在服务端保存会话。
JWT 实现不复杂,但有三个点要提前设计好。第一是密钥,不要硬编码在业务代码里,放到配置文件里,每个环境用不同密钥。第二是过期时间,一般设 2 小时左右,演示当天为了避免 Token 过期,可以临时调成 12 小时或更长。第三是拦截器的放行规则,登录接口、注册接口、静态资源要放行,其余接口都要校验,这个规则写错了会出现"前端明明登录了,但请求总是 401"的怪问题。
2.4 数据库:MySQL 就够了,别硬塞中间件
小型房屋租赁系统用 MySQL 单库完全够用。很多同学为了"显得高级",在毕设里引入 Redis 缓存、MQ 消息队列,我不推荐。引入中间件意味着你必须在文档和答辩里解释它的作用、原理、故障处理方式,老师顺着你的话连续追问几下,你答得稍微含糊,反而暴露短板。把 MySQL 表结构设计好、索引建对、事务用对,已经能达到良好的标准。
存储引擎统一 InnoDB,字符集统一 utf8mb4。金额字段不要用 float 或 double,用 decimal(10,2),否则算账的时候精度问题会让你非常尴尬。
3. 数据库建模:四张核心表怎么设计
3.1 用户表:角色权限从这里开始
房屋租赁系统里有管理员、房东、租客三类角色。用户表不一定要做复杂的 RBAC 权限模型,但至少要有一个 role 字段,并且在前后端都做好角色判断。一个典型的用户表结构是这样的:
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '密码,BCrypt加密存储',
`real_name` varchar(50) NOT NULL COMMENT '真实姓名',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`role` tinyint NOT NULL DEFAULT '3' COMMENT '角色:1管理员 2房东 3租客',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1正常 0禁用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
注意 password 字段不要用 varchar(32) 存 MD5。BCrypt 加密后的结果是固定 60 位,所以密码字段长度至少要 60。MD5 很容易被彩虹表反查,答辩老师基本都会追问"密码为什么加密、用的是什么算法",你提前想清楚"为什么用 BCrypt",反而能把这个提问变成加分项。
3.2 房屋表:状态字段是整个业务的核心
房屋表是核心资产表,字段比较多,至少包括房源编号、标题、地址、户型、面积、朝向、月租金、押金、图片、状态、所属房东 ID、创建时间。最关键的是 status 这个字段:
sql复制`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1待出租 2已预订 3已出租 4维修中 5已下架'
这个状态字段是全系统的业务中枢。房源上架时是"待出租",租客预约后变成"已预订",签约成功后变成"已出租",到期或退租后回到"待出租",中间还可能插入"维修中"和"已下架"。状态不能随意跳转,要由后端 Service 层统一控制,不能前端传什么值就直接更新表结构。
写代码时,建议把状态的合法流转画成一张状态转移图放进文档里。比如"已预订"只能流转到"已出租"或"待出租",不能直接跳到"维修中"。答辩时把这张图一展示,老师马上就知道你做过完整的状态设计,而不是写了个"改状态"按钮。
3.3 合同表:押金、计费方式要留足字段
租赁合同表记录一次租赁关系的核心信息。基本字段包括合同编号、房屋 ID、租客 ID、房东 ID、租赁开始时间、结束时间、月租金、押金、计费方式、合同状态。参考建表语句如下:
sql复制CREATE TABLE `contract` (
`id` bigint NOT NULL AUTO_INCREMENT,
`contract_no` varchar(32) NOT NULL COMMENT '合同编号',
`house_id` bigint NOT NULL COMMENT '房屋ID',
`tenant_id` bigint NOT NULL COMMENT '租客用户ID',
`owner_id` bigint NOT NULL COMMENT '房东用户ID',
`start_date` date NOT NULL COMMENT '起租日期',
`end_date` date NOT NULL COMMENT '到期日期',
`rent_amount` decimal(10,2) NOT NULL COMMENT '月租金',
`deposit` decimal(10,2) NOT NULL COMMENT '押金',
`pay_type` tinyint NOT NULL COMMENT '计费方式:1月付 2季付 3年付',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1生效中 2已到期 3已退租',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_contract_no` (`contract_no`),
KEY `idx_house_id` (`house_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租赁合同表';
合同编号建议自己生成,不要直接用数据库自增 ID 裸奔。比如"HT" + 日期 + 随机数,生成的规则写在代码里。索引方面,房屋 ID 和租客 ID 都是高频查询字段,必须加索引,否则数据量稍微上来就会变慢。
3.4 账单表:让系统自动算账,而不是人工记账
账单表负责记录租金和水电费等费用明细。合同生效后,系统按照计费周期自动生成账单。字段包括账单编号、合同 ID、房屋 ID、租客 ID、费用类型、金额、生成日期、支付状态、支付时间、支付方式。
生成账单的常见做法有两种:一种是签约时按计费周期一次性生成未来所有账单;另一种是提供"生成本月账单"的接口,每月触发一次。毕设阶段建议用第一种:签约成功时,把合同期内每个计费周期的账单全部生成出来,状态设为"待支付"。这样做的好处是逻辑简单清晰,不需要引入定时任务,演示时也能直观看到"签约后立刻出现多期账单"。
这里有一个非常容易被忽略的细节:合同起止日期往往不是自然月。比如 5 月 15 日到 8 月 14 日,第一期账单应该怎么算?简单做法是按天折算:月租金 ÷ 30 × 实际天数,保留两位小数。这个计算规则不复杂,但一定要在文档里写清楚公式,否则答辩时老师随便举一个跨月日期,你可能当场算错。
4. 后端核心逻辑:不是增删改查的三块硬骨头
4.1 登录鉴权:拦截器里放行路径要仔细核对
后端最基础但又最容易出问题的,是登录鉴权。用 JWT 的思路是:写一个拦截器实现 HandlerInterceptor,在 preHandle 里从请求头取 Token,解析并验证签名和有效期,失败就返回 401。放行路径必须包括登录接口、注册接口、静态资源。
一个常见的坑是:前端页面加载时,路由守卫后会先请求"获取当前用户信息"的接口,如果这个接口没有放行,前端就会陷入"登录成功但页面一直转圈"的尴尬循环。所以调试时建议先用 Postman 把所有接口测一遍,再联调前端。
拦截器拿到 Token 后,要把用户信息放进请求上下文里,比如放在 ThreadLocal 或 request attribute 里,后续 Service 层通过工具方法获取当前登录用户 ID。不要在业务代码里到处解析 Token,那样代码会散落得不成样子。
4.2 防止一房多租:状态检查 + 事务加锁缺一不可
这是整个系统最值得讲的业务点。
假设两个租客同时对同一个房子发起租赁申请,系统怎么保证不会签出两份合同?两步走。
第一步是状态前置检查。提交订单时,先查询房屋状态是否等于"待出租",只有待出租状态才允许创建预订记录,并把房屋状态改为"已预订"。这一步应该在 Service 层完成,不要放在 Controller 里。
第二步是事务和数据一致性。创建订单和修改房屋状态要放在同一个事务里,并且使用行级锁。简单做法是 select ... for update,把对应房屋行锁住,事务提交后再释放。伪代码逻辑大致是这样:
java复制@Transactional(rollbackFor = Exception.class)
public void createOrder(Long houseId, Long tenantId) {
// 锁住房源行
House house = houseMapper.selectByPrimaryKeyForUpdate(houseId);
if (!"1".equals(house.getStatus())) {
throw new BizException("该房源已被预订或已出租");
}
// 创建订单
orderMapper.insert(buildOrder(house, tenantId));
// 更新房源状态为已预订
house.setStatus("2");
houseMapper.updateById(house);
}
用 select ... for update 时要注意:查询条件必须走主键或索引,否则会锁住整个表。比如 where id = #{id},因为 id 是主键,锁的只是这一行;如果写成 where owner_id = xxx,可能把这位房东名下的所有房子都锁一遍,性能和可靠性都受影响。
这个知识点只要讲清楚,答辩时从"并发控制"切入,老师的观感会好很多,因为它证明了你真的考虑过多人同时操作的问题。
4.3 合同签约与账单生成:事务边界要画清楚
合同签约是整套系统最复杂的操作之一。它要连续做几件事:校验订单状态、校验房屋状态、生成合同、把房屋状态改为已出租、生成第一期账单。这几步必须放在同一个事务里,任何一步失败都要整体回滚。
账目计算尤其要注意精度。月租金是 decimal 类型,代码运算时必须用 BigDecimal,不能用 double。比如月租金 3000 元,从 5 月 15 日起租到月底,第一期租金可以这样折算:
java复制BigDecimal monthlyRent = new BigDecimal("3000");
BigDecimal dailyRent = monthlyRent.divide(new BigDecimal("30"), 2, RoundingMode.HALF_UP);
BigDecimal periodRent = dailyRent.multiply(new BigDecimal("17")).setScale(2, RoundingMode.HALF_UP);
这里有个容易踩的坑:monthlyRent.divide(...) 如果除不尽且没指定精度和舍入模式,会直接抛异常。所以一定要指定除法的精度和 RoundingMode.HALF_UP。文档里要注明"合同按每月 30 天折算",这样计算口径就是自洽的。
账单生成之后,再提供"支付账单"的模拟接口,修改账单状态为已支付,记录支付时间和支付方式。不用对接真实第三方支付,模拟即可,但接口语义要规范,比如 POST /bill/{id}/pay。
4.4 接口统一返回结构:一个小习惯,答辩也能讲
后端接口建议统一返回 Result 对象,包含 code、msg、data 三个字段。比如登录成功返回 { "code": 200, "msg": "OK", "data": { token, userInfo } },失败返回 { "code": 500, "msg": "密码错误" }。这样前端只需要做一层拦截处理,不需要每个接口各自定义返回格式。
这个习惯看起来不起眼,但写进文档里、答辩时提一句"我统一了接口返回格式,前端只需处理一套错误码",就体现了工程意识。很多没做过完整项目的同学不会注意这个细节,你做了就是亮点。
5. 前端 Vue:几个值得写进简历的细节
5.1 路由守卫与动态菜单:从根源上处理登录态
前端最基础但最重要的功能是路由守卫。核心需求有两个:用户未登录时,访问任何页面都跳转登录页;登录后根据角色显示不同菜单。路由守卫的写法不复杂:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path === '/login') {
next()
return
}
if (!token) {
next('/login')
return
}
next()
})
动态菜单可以在登录后请求一次"获取当前用户信息"接口,拿到 role 字段,按角色渲染菜单。管理员看到用户管理和基础数据配置,房东看到房源管理和合同列表,租客看到找房和我的订单。
这里要强调一点:权限控制绝不能只靠前端隐藏菜单。后端接口同样要有角色校验,前端只是一层体验上的过滤,真正的安全边界必须由后端来守。这个"前后端双保险"的思路写进文档里,是标准的加分表达。
5.2 表单校验:手机号和身份证不能只做非空判断
租房场景里有几个绕不开的表单校验:手机号、身份证号、租赁时间段。如果不做格式校验,用户填一个乱码身份证号,后面的合同和账单就会全部跟着出错。
Vue + Element UI 里用 el-form 的 rules 即可。手机号的正则校验可以写成这样:
javascript复制const validatePhone = (rule, value, callback) => {
if (!/^1[3-9]\d{9}$/.test(value)) {
callback(new Error('手机号格式不正确'))
} else {
callback()
}
}
身份证号校验更严格,需要校验前 17 位和最后一位校验码。网上有很多现成实现,但建议自己手写几遍。答辩时如果被问"正则为什么这么写""校验码怎么算",你能当场说清楚,绝对会是加分项。
5.3 数据可视化:出租率和收入趋势让界面活起来
如果整个前端只有表格和表单,演示时看起来会很干。加两个图表,效果完全不一样。
- 房屋出租率:用 ECharts 的饼图或环形图,展示当前所有房屋的状态分布,比如出租中、待出租、维修中的比例。
- 月度收入趋势:用折线图展示近 6 个月每个月的租金收入汇总。
ECharts 的引入本身不难。关键点是图表数据要从后端接口拿,不要写死在页面上。比如设计一个统计接口,返回近 6 个月的账单金额汇总,前端调用后渲染成折线图。这样一个"后端算好数据、前端负责可视化"的小闭环,正好体现你对前后端分离的理解。
6. 从零到能演示:环境搭建与部署全过程
6.1 版本清单:踩坑最少的组合
无论 Windows 还是 macOS,按下面的组合来做,基本不会遇到大坑:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 与 Spring Boot 2.7.x 配合最稳定 |
| Maven | 3.6.3 及以上 | IDEA 自带 Maven 也可以 |
| MySQL | 5.7 或 8.0 | 8.0 需使用新驱动类名 |
| Node.js | 14.x 或 16.x | 前端 npm 命令需要,版本太高可能出现警告 |
| Vue CLI | 4.x 或 5.x | 用于创建和启动前端项目 |
6.2 后端配置与启动
第一步,创建数据库,执行项目提供的 .sql 脚本,生成表结构和初始化数据。别跳过初始化数据,否则登录时连管理员账号都没有。
第二步,修改 resources/application.yml 里的数据库连接配置:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/house_rent?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 你的密码
driver-class-name: com.mysql.cj.jdbc.Driver
注意 driver-class-name。MySQL 8.0 推荐 com.mysql.cj.jdbc.Driver,MySQL 5.7 用 com.mysql.jdbc.Driver 或新版驱动都行,关键是驱动包的 Maven 依赖版本要和数据库版本匹配。
第三步,用 IDEA 打开后端工程,等 Maven 下载依赖,然后运行主类。看到 "Tomcat started on port(s): 8080" 就说明后端起来了。
6.3 前端配置与启动
第一步,打开前端项目,执行 npm install 安装依赖。如果下载很慢,先配置国内 npm 镜像。
第二步,修改 vue.config.js 里的 devServer.proxy,让前端请求代理到后端地址:
javascript复制devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
这里有个细节很容易出错:如果后端设置了 server.servlet.context-path: /api,那前端请求直接发 /api 就可以了;如果后端没有设置这个前缀,前端代理里就不要再自己加 /api,否则会叠出一个不存在的 /api/api 路径,产生 404。
第三步,执行 npm run serve 启动前端,访问 localhost 对应的端口,用初始化账号登录。比如 admin / 123456,具体以项目脚本为准。
6.4 常见启动报错对照表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 后端启动报数据库连接失败 | 密码写错或数据库没创建 | 检查 application.yml,先执行 sql 脚本 |
| 启动时提示端口被占用 | 8080 被其他程序占用 | 换一个端口,或找到占用进程并结束 |
| npm install 极慢或卡住 | 网络原因 | 切换国内 npm 镜像,再重新执行 |
| 页面白屏、控制台请求 404 | 前端代理路径和后端接口前缀不一致 | 统一接口前缀,仔细核对代理配置 |
| Token 总是提示过期 | 系统时间或时区不一致 | 统一使用 Asia/Shanghai 时区 |
7. 毕业设计最容易翻车的 5 个答辩问题
7.1 "为什么用 JWT 而不用 Session?"
这道题几乎必问,建议提前准备好回答。可以从这几个角度讲:一是前后端分离场景下,后端做成无状态,扩展时更方便;二是 Session 需要维护服务端会话,跨域场景下还要处理 Cookie 携带的问题;三是 JWT 把身份信息放在客户端,服务端只校验签名。
同时你也要能说出 JWT 的缺点,比如 Token 过期前无法主动失效、密钥一旦泄露风险很大。能说出两面,说明你是真的理解,而不是背了网上的答案。
7.2 "系统怎么保证一套房不会被租两次?"
把状态机和事务控制讲清楚:房屋状态由后端统一管理,创建订单时用行锁锁住房屋记录,状态不是"待出租"就抛出异常。如果老师追问"为什么不直接在数据库加唯一索引",你要能解释:一房多租的本质是并发修改同一行状态,不是重复插入问题,所以需要锁和事务配合。
7.3 "为什么密码要加密存储?MD5 为什么不够好?"
这道题要把本质答出来。MD5 是哈希算法,不需要密钥,而且相同输入永远得到相同输出,彩虹表可以直接反查常见弱密码。BCrypt 是专门为密码设计的慢哈希算法,自带随机盐,同一个密码每次加密的结果都不同,暴力破解成本高很多。这套解释写进文档里,答辩时直接照着逻辑讲就行。
7.4 "数据库表之间为什么没有用外键?"
两种说法都能站住脚。如果你加了外键,就说"外键保证数据完整性,比如合同表引用房屋表时会校验房屋是否存在";如果你没加外键,可以说"外键在高并发写入场景下会影响性能,互联网项目里普遍采用逻辑外键,由业务层保证一致性,我在 Service 层做了前置校验"。最怕的是你从来没想过这个问题,老师一问就愣住。
7.5 "这个系统如果做大,怎么扩展成微服务?"
不需要你真的在代码里引入微服务,但要有拆分思路。可以这样讲:把用户、房屋、合同、账单拆成独立服务,每个服务独立数据库,服务之间通过接口通信;热门房源数据可以先做 Redis 缓存,账单生成压力大时可以用队列异步化处理。只要思路清晰,老师就会认为你有架构意识。
8. 文档与演示环节的一点小建议
8.1 文档侧重点:少写操作手册,多写设计理由
很多同学项目做得好,文档却写成"点击按钮 A,弹出对话框 B"的操作手册,非常可惜。我认为文档至少要包含三个部分:需求分析,明确三个角色的功能清单和权限矩阵;数据库设计,附上 ER 图和每张表字段说明;核心逻辑说明,重点写状态流转、合同计费、并发防重复方案。
篇幅不用追求特别长,但要把"为什么这样设计"写清楚。老师看文档时,最想看到的是设计背后的思考,而不是操作步骤的堆砌。
8.2 演示顺序:走一条完整业务链路
演示时不要东点一下西点一下。建议按这种顺序:先展示登录和不同角色菜单的差异;再走一条完整业务链路,比如"管理员发布房源→租客预约→房东确认→合同签约→生成账单→支付模拟→退租结算"。这条链路走完,系统的核心功能基本都覆盖了,老师对这个项目的印象也会非常完整。
8.3 最后一点心得
我带过的同学里,复现这个类型项目时出问题最多的其实不是代码,而是"讲不清楚"。源码可以先用现成的,但合同计算、状态流转、权限拦截这三块,一定要自己手写一遍。写熟了,你自然就敢在老师面前拆开讲。
还有一个很实际的经验:演示环境准备两条路。一条是本机运行,另一条是提前录好完整的操作视频。万一现场设备临时出问题,你还有后备方案,这个细节真的能在关键时刻救场。
