先直接说结论:这套基于 Spring Boot + Vue 的房屋租赁管理系统,是我个人完整跑通过、并且整理成“源码 + 文档”交付过的一个全栈项目。它解决的问题很实际——传统的租房流程里,房东要贴广告、租客要到处跑腿看房,合同、水电费账单、退租押金这些全靠纸笔和脑子记账,一到月底对账就混乱。系统把房源发布、在线预约看房、合同签订、账单缴费、退租清算这几个环节全部收到线上,房东和租客各有一个端,职责清晰,数据留痕。如果你正在做课程设计、毕业设计,或者想弄一套能写的全栈项目经验,这套基于 Spring Boot 后端、Vue 前端的租赁系统源码和文档,是非常典型的一套参考案例。接下来的内容,我会把从设计思路到表结构、从后端接口到前端页面、从本地联调到部署上线的完整路径拆开讲,包括我实际踩过的坑。
1. 先聊项目定位与整体设计
1.1 房屋租赁管理系统到底解决什么问题
很多人在需求分析阶段就卡住了,不知道一个租赁管理系统该有哪些功能。我当时接手这个项目时,第一件事不是打开 IDE,而是先把现实中租房的全流程梳理了一遍:房东发布房源 → 租客搜索浏览 → 预约实地看房 → 双方确认意向 → 签订租赁合同 → 按周期缴纳租金和水电费 → 到期退租,结算押金和欠费。
这七个环节,每个环节都涉及数据记录和状态变化。例如“预约看房”,租客提交预约后,房东需要有一个待确认列表;预约通过后,租客才能联系房东或按约定时间上门。“合同签订”则意味着租客和房源要绑定,房源状态要从“空置”变为“已租”。缺了这些状态流转,系统就只是摆设。
所以我最后确定的核心功能是两大类:
- 租客端:注册登录、房源搜索、查看房源详情、提交预约、查看我的预约、签订合同、查看账单并缴费、查看个人租赁记录。
- 管理端(房东/管理员):房源管理(增删改查、上下架)、预约审核、合同管理、账单管理(自动生成、手动调整)、租客档案、公告发布、数据统计。
这套功能划分,既覆盖了业务主流程,又保留了每个模块的独立性。对毕设而言,它可以自然引出“权限管理”“状态机”“定时任务生成账单”等多个可深挖的答辩话题。
1.2 技术选型:为什么是 Spring Boot + Vue
市面上能做全栈的技术组合很多,但 Spring Boot + Vue 几乎是当前最容易找到参考资料、最容易招人/过答辩的组合。原因有三:
第一,生态成熟。 Spring Boot 2.7.x 依然是国内大量教学和中小型项目的首选版本,资料多,排错成本低。我见过很多人因为标题里写着“springboot”,结果选了个 3.x 甚至 4.x 的最新版,然后发现 MyBatis-Plus、Shiro、旧版文档全部对不上,白白花掉几天时间。真正做项目时,稳定版本比最新版本更有价值。
第二,前后端分离的开发模式清晰。 Spring Boot 只负责提供 RESTful API,Vue 负责页面渲染和交互,两边通过 JSON 通信。这样的好处是,数据库逻辑和页面逻辑互不干扰,出了问题可以快速定位到某一端。对个人开发来说,虽然要多学一个前端框架,但 Vue 的学习曲线相对平缓,模板语法简单,组件化思维和小程序开发也有共通之处。
第三,就业和答辩认可度高。 Java 后端是招聘市场的绝对主力,Vue 则是国内中小型公司使用最广的前端框架之一。你拿这套项目去面试,面试官几乎不用额外了解背景就能理解你的技术栈。如果你后续想扩展,比如接入地图找房、接入支付,社区里都有现成的解决方案。
1.3 功能模块全景图
我最终设计的模块划分如下,你可以把它当作需求文档的起点:
| 模块 | 租客端 | 管理端 |
|---|---|---|
| 账号与权限 | 注册、登录、个人信息 | 管理员登录、租客账号管理 |
| 房源中心 | 浏览出租房源、搜索/筛选 | 房源发布、编辑、上下架 |
| 预约管理 | 提交看房预约、取消 | 查看预约、通过/拒绝 |
| 合同管理 | 查看电子合同、确认签约 | 生成合同、合同列表 |
| 账单管理 | 查看账单、在线缴费标记 | 生成账单、实收登记 |
| 公告中心 | 查看公告 | 发布公告 |
这里需要注意一个细节:合同不能简单由租客直接创建,必须是管理员根据“房源 + 租客 + 租金”三个要素来生成。我在实现时才意识到,如果让租客自己签合同,那租金标准、押金比例、起止日期就缺少校验,很容易产生脏数据。所以设计上,租客端只能“确认/查看合同”,管理端才有“起草合同”的权限。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:这套系统的“地基”
2.1 核心业务表规划
数据库永远是这类业务系统的核心,表设计好了,后端代码写起来非常顺;表设计烂,后面每一步都在还债。我建了下面这些表:
user:登录账号表,区分 role(租客/管理员)house:房源信息表,包含标题、描述、租金、户型、面积、地址、经纬度、状态house_image:房源图片表,一个房源多张图片appointment:看房预约表,关联 user_id 和 house_idcontract:合同表,关联 user_id 和 house_id,保存起止日期、租金、押金、状态bill:账单表,关联 contract_id,记录每月应缴费用notice:公告表feedback:反馈建议表(可选)
细心的你可能发现了,账单表关联的是合同而不是房源。这是我在第一个版本里踩过的坑:如果账单关联到“租客 + 房源”,同一个房子换租客后,历史账单和现任租客账单就会混淆。关联合同后,合同本身已经绑定了租客和房源,账单按合同归属就永远不会错。
2.2 关键表结构 SQL
拿最核心的 house 表和 bill 表举例。房源表这样建:
sql复制CREATE TABLE `house` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '房源标题',
`description` text COMMENT '房源描述',
`cover` varchar(255) DEFAULT NULL COMMENT '封面图URL',
`province` varchar(50) DEFAULT NULL,
`city` varchar(50) DEFAULT NULL,
`area` varchar(100) DEFAULT NULL COMMENT '区域/小区',
`address` varchar(255) DEFAULT NULL COMMENT '详细地址',
`price` decimal(10,2) NOT NULL COMMENT '月租金',
`deposit` decimal(10,2) DEFAULT NULL COMMENT '押金',
`house_type` varchar(20) DEFAULT NULL COMMENT '户型,如两室一厅',
`area_size` decimal(10,2) DEFAULT NULL COMMENT '面积㎡',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0空置 1已租 2下架',
`publish_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房源信息表';
金额字段我用的是 decimal(10,2),而不是 float 或 double。这是做财务相关系统的铁律,浮点数在计算时会出现精度丢失,房租、押金涉及真金白银,绝对不能出现 99.99 变成 99.99000001 的情况。
账单表是另一个容易出问题的表,我设计的核心字段如下:
sql复制CREATE TABLE `bill` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`contract_id` bigint(20) NOT NULL COMMENT '合同ID',
`period` varchar(10) NOT NULL COMMENT '账期,如2025-06',
`rent_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '租金',
`water_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '水费',
`electric_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '电费',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待缴费 1已缴费 2已逾期',
`create_time` datetime DEFAULT NULL,
`pay_time` datetime DEFAULT NULL COMMENT '缴费时间',
PRIMARY KEY (`id`),
KEY `idx_contract_period` (`contract_id`, `period`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账单表';
我加了 idx_contract_period 联合索引,因为业务上最常见的查询是“某个合同在某个账期是否存在账单”。如果没有这个唯一性意识,重复生成账单就会出现两条同样收费记录,非常尴尬。
2.3 表关系设计思路与常见坑
表与表之间的关系并不复杂:用户和房源是一对多(一个房东多套房源),用户和预约是一对多,房源和图片是一对多,合同和账单是一对多。需要注意的地方在于:
合同表中应该冗余快照字段。 房租可能会调整,如果合同里只存 house_id,将来房源租金改了,你再去算历史账单,就会用错数据。我的解决方案是:在 contract 表里冗余了 rent_amount、deposit_amount、start_date、end_date 这些字段,生成合同时就把当时的价格存死,之后房源价格怎么调,都不影响已生效合同。
用户表不要和租客表混为一谈。 我最初的设计里只有一个 user 表,后来发现管理员和租客需要的字段完全不同(管理员不需要身份证号,租客不需要角色权限分组)。最后拆成两个表来理解:user 只负责“登录认证”,再补充个人资料字段即可,核心数据不要过多冗余。
3. 后端核心逻辑与接口实现
3.1 项目分层与目录结构
后端我的目录结构遵循了标准的三层架构,并在此基础上加了 controller – service – mapper 的拆分:
code复制src/main/java/com/example/rent
├── controller # 控制层,只做参数接收和结果封装
├── service # 业务层,存放具体逻辑
├── mapper # 数据访问层,MyBatis-Plus 的 Mapper 接口
├── entity # 数据库实体类
├── dto # 接收参数的封装类
├── vo # 返回前端的结果封装类
├── config # 配置类,如跨域、拦截器
├── utils # 工具类,如 JWT、日期工具
└── common # 统一返回结果、异常处理
这样的分层最大的好处是“职责单一”。控制器保持精简,只负责参数绑定和调用 service;业务逻辑全部集中在 service,方便写单元测试;数据访问层被 MyBatis-Plus 接管后,普通的增删改查几乎不需要手写 SQL。
依赖方面,核心就是这几个:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
注意,我没有用 Spring Boot 自带的 JPA,而是选了 MyBatis-Plus,原因很简单:JPA 的自动建表、懒加载机制对于新手来说黑盒程度太高,出了问题不好排查;MyBatis-Plus 的 CRUD 方法非常直白,复杂查询再写 XML,学习成本更低,而且国内公司用 MyBatis 系的比例明显更高。
3.2 登录鉴权与 JWT 实现
登录鉴权是几乎所有全栈项目的标配。我用的是 JWT(JSON Web Token),核心思路是:用户登录成功后,后端签发一个 token 返回前端;前端之后每次请求都带上这个 token;后端通过拦截器解析 token,获取当前用户信息。
JWT 的操作步骤拆开来说:
- 登录接口校验用户名密码。
- 密码用 BCrypt 加密存储,一定不能明文存取。注意 BCrypt 每次加密结果都不同,所以不能用“加密后相等”来判断,而是用
BCryptPasswordEncoder.matches(原始密码, 数据库密文)。 - 签发 token,载荷部分存 userId 和 role,设置过期时间。
- 写一个拦截器,对 /api/** 下的请求统一校验 token。
关键代码大致是这样:
java复制@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true; // 预检请求直接放行
}
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new BusinessException(401, "未登录或登录已过期");
}
try {
DecodedJWT jwt = JWT.require(Algorithm.HMAC256(SECRET))
.build()
.verify(token.replace("Bearer ", ""));
request.setAttribute("userId", jwt.getClaim("userId").asLong());
request.setAttribute("role", jwt.getClaim("role").asString());
return true;
} catch (JWTVerificationException e) {
throw new BusinessException(401, "token无效");
}
}
}
这里有个实际开发中很容易忽略的问题:OPTIONS 预检请求必须放行。 前后端分离环境下,如果前端请求带了 Authorization 头,浏览器会先发一个 OPTIONS 请求问后端“允不允许这个跨域请求”。如果拦截器直接把 OPTIONS 拦截了,前端会一直报跨域错误,但在后端日志里根本看不到业务日志。这个坑我帮不少同学排查过,基本都是漏了上面这行判断。
3.3 租赁核心接口:合同、账单
合同和账单是两个最体现业务逻辑的接口,写得好不好,直接决定系统能不能“用起来”。
生成合同的处理逻辑:
java复制public Contract createContract(CreateContractDTO dto) {
House house = houseMapper.selectById(dto.getHouseId());
if (house == null || house.getStatus() != 0) {
throw new BusinessException("该房源不存在或已出租");
}
Contract contract = new Contract();
contract.setUserId(dto.getUserId());
contract.setHouseId(house.getId());
contract.setTitle(house.getTitle());
contract.setRentAmount(house.getPrice());
contract.setDepositAmount(dto.getDepositAmount());
contract.setStartDate(dto.getStartDate());
contract.setEndDate(dto.getEndDate());
contract.setStatus(0); // 待租客确认
contractMapper.insert(contract);
// 房源状态改为已租
house.setStatus(1);
houseMapper.updateById(house);
return contract;
}
我在第一版里犯过一个错误:插入合同后忘了同步修改房源状态,导致同一套房源可以被重复签约。后来在 service 层里加上状态校验和状态流转,才算真正闭环。
账单生成逻辑: 账单不应该手动一个个插入,而应该在合同生效后,按照租期自动生成。一开始我用的是定时任务,每天扫描一次合同,看到“当天是每月固定日期”就生成账单。后来发现这个方案不太好控制(比如合同是 15 号签的,账单周期怎么算?换租客后旧账单谁来管?),最终改成了“按账期生成”:查询所有 status = 1 且已生效的合同,判断该合同在当前账期是否已有账单,没有则补生成。这样逻辑简单,也方便答辩时讲清楚你的“账期”概念。
4. 前端 Vue 工程搭建与核心页面
4.1 环境安装与工程初始化
前端我选择的是 Vue 2.6 + Element UI,不是 Vue 3 + Element Plus。原因是:宿舍、毕设、参考文档,几乎 70% 以上都基于 Vue 2,遇到问题搜答案更快。当然如果你已经熟悉 Vue 3,直接上 Vue 3 也没问题,核心思路一样。
环境准备分三步:
bash复制# 1. 安装 Node.js(建议 14.x 或 16.x,不要用最新版)
# 2. 安装脚手架
npm install -g @vue/cli
# 3. 创建工程
vue create rent-front
# 选择 Manually select features,勾选 Router、Vuex、Axios
然后安装 UI 组件库和网络请求库:
bash复制npm install element-ui axios --save
这里有个版本兼容的大坑:Node.js 版本太高,例如 Node 18 以上,老项目的依赖很容易出现 opensslErrorStack 或构建内存溢出。我自己的建议:如果跑的是 Vue 2 项目,node 14 是最稳的;如果想用新语法,干脆直接上 Vue 3,不要在一个 Vue 2 老项目里硬刚新环境。
4.2 路由与请求封装
工程的 src 目录下我做了这些划分:
code复制src
├── api # 封装的 axios 接口
├── assets # 静态资源
├── components # 公共组件
├── router # 路由配置
├── store # 全局状态
└── views # 页面视图
路由配置里用了嵌套路由和路由守卫。租房端和管理端是两种完全不同的布局,所以我把路由拆成了两组:
javascript复制const router = new VueRouter({
routes: [
{
path: '/',
component: HomeLayout,
children: [
{ path: '', redirect: '/house/list' },
{ path: 'house/list', component: HouseList },
{ path: 'house/detail/:id', component: HouseDetail },
{ path: 'my/appointment', component: MyAppointment },
{ path: 'my/contract', component: MyContract },
{ path: 'my/bill', component: MyBill }
]
},
{
path: '/admin',
component: AdminLayout,
meta: { requireAdmin: true },
children: [
{ path: 'house', component: AdminHouse },
{ path: 'appointment', component: AdminAppointment },
{ path: 'contract', component: AdminContract },
{ path: 'bill', component: AdminBill }
]
}
]
})
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requireAdmin && !token) {
next('/login')
} else {
next()
}
})
路由守卫是我觉得全栈项目里必写的一笔。它能在前端层面拦截未登录用户,虽然不能替代后端权限校验,但能极大提升用户体验——不需要等接口返回 401 才跳转登录页。
axios 请求封装方面,我做了两件小事:第一,拦截器统一从 localStorage 取 token,加到请求头里;第二,响应拦截器统一处理 401 状态码,跳转登录页并清理本地信息。
4.3 核心页面实现细节
房源列表页是整个项目前端最核心的页面。它承担了三个职责:展示房源卡片、根据关键词搜索、按租金/户型筛选。我实现时没有用复杂的状态管理,而是用一个 query 对象统一驱动:
javascript复制data() {
return {
query: {
keyword: '',
maxPrice: null,
houseType: '',
current: 1,
size: 8
},
list: [],
total: 0
}
},
methods: {
async fetchList() {
const res = await searchHouse(this.query)
this.list = res.data.records
this.total = res.data.total
}
}
合同确认页要注意的是合同详情信息的展示格式。后端返回的日期是 Date 对象,如果直接用 {{ contract.startDate }} 展示,会出现“2025-06-01T00:00:00.000+00:00”这种串,很不美观。我的处理方式是在后端就统一返回格式化后的字符串,比如 2025-06-01,前端不处理格式化逻辑,减少出错点。
账单缴费页我做了两件事:待缴费账单用醒目颜色标识;已缴费账单显示缴费时间和账单详情。缴费按钮点击后调后端接口,成功后刷新列表。这里不做真实支付,只做一个“模拟缴费”的状态更新,对于毕设和演示已经完全够用。
5. 联调部署与高频问题排查
5.1 前后端联调设置
本地开发时,前端跑在 8080 端口,后端跑在 9090 端口,存在跨域问题。最常见的解决方案有两种:
方案一:后端开启全局跨域配置。 在 Spring Boot 里加一个配置类:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
要注意的是,allowCredentials(true) 时 allowedOrigin 不能直接用 *,得用 allowedOriginPatterns("*")。否则浏览器会直接拒绝请求。
方案二:前端 devServer 配置代理。 在 vue.config.js 里这样写:
javascript复制module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:9090',
changeOrigin: true
}
}
}
}
这两种方案我建议同时掌握。联调时用后端跨域配置最省事;部署时前端打包成静态资源后,交给 Nginx 转发,不再依赖后端跨域。实际项目里,跨域问题十有八九是配置细节出错,常见的表现就是“前端报跨域,后端日志里有请求记录但没业务日志”,这时候优先检查拦截器有没有放行 OPTIONS。
5.2 打包部署到服务器
部署流程我梳理成一条线:后端打成 jar 包,前端打成 dist 目录,然后把 dist 扔进 Nginx 的 html 目录,Nginx 再反向代理 /api 到后端的 9090 端口。
后端打包命令:
bash复制mvn clean package -DskipTests
# 生成 target/rent-system.jar
nohup java -jar rent-system.jar --server.port=9090 > rent.log 2>&1 &
前端打包命令:
bash复制npm run build
# 生成 dist 目录
Nginx 关键配置:
nginx复制server {
listen 80;
server_name your-domain.com;
location / {
root /usr/share/nginx/html/dist;
index index.html;
try_files $uri $uri/ /index.html; # 解决刷新404
}
location /api/ {
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
这里最值得强调的就是 try_files $uri $uri/ /index.html; 这一行。Vue Router 默认使用 history 模式,路由路径是真实的 URL,比如 /house/detail/1。如果不加这一行,用户直接刷新这个页面时,Nginx 会到磁盘找对应的文件,找不到就返回 404。加了这个配置后,所有未命中静态资源的请求都会回退到 index.html,由 Vue Router 自己判断路由。这个坑几乎每个部署 Vue 项目的人都会碰到,务必记住。
5.3 高频问题排查清单
我整理了一份实际操作中遇到最多的问题清单,按照频率排序:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 前端请求后端一直 404 | 前端请求路径写错,或后端 context-path 不一致 | 统一在请求路径前加 /api,后端 controller 的 @RequestMapping 路径和前端 api 模块保持一致 |
| 登录成功但列表接口报 401 | JWT 过期时间太短,或 token 没被正确携带 | 检查 axios 拦截器是否取到了 localStorage 的 token,确认请求头格式是否为 Authorization: Bearer xxx |
| 新增房源后图片不显示 | 图片路径存的是本地绝对路径,前端无法访问 | 后端配置静态资源映射:registry.addResourceHandlers,把本地目录映射到 /upload/** 访问路径 |
| 合同日期显示乱码或时区差 8 小时 | 后端返回 Date 对象,前端解析时区不对 | 后端用 @JsonFormat(pattern="yyyy-MM-dd", timezone="GMT+8") 统一格式化 |
| 部署后界面样式丢失 | Nginx root 路径指向错误,或静态资源路径是绝对路径 | 检查 dist 文件位置,确认 Vue 的 publicPath 是否设置为 ./ 或 / |
| 数据库中文乱码 | 建库时字符集不是 utf8mb4 | 建库语句加 DEFAULT CHARACTER SET utf8mb4,并在连接串上加 characterEncoding=utf8 |
最后再说一些个人体会。这套基于 Spring Boot + Vue 的房屋租赁管理系统,真正花时间的地方不在“写代码”,而在“想清楚业务规则”。比如合同还没确认时房源状态到底算不算已租?账单是生成后才允许缴费还是缴完费再生成?这些规则如果不在表设计和接口实现前定好,写出来的代码只能是表面完整,一测就散。我自己在做第一版时就经历了反复改表结构的阶段,后面沉下心花了半天把状态流转画清楚,开发效率才提上来。建议你也这样:拿到源码先别急着跑,先用文档把流程捋一遍,然后再动手。这样无论是跑通项目还是答辩讲解,都会顺很多。
