ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前后端分离的房屋租赁管理系统设计与实现教程

前后端分离的房屋租赁管理系统设计与实现教程

做房屋租赁管理系统这类型项目,我前后折腾过好几版。最早是单体结构,JSP 页面套后端,改个样式要重启服务,加个功能动辄牵一发动全身。后来公司接了个真实需求,要求把房源发布、租客签约、账单催缴、合同管理都做成前后端分离的 Web 系统,我索性用 SpringBoot + Vue + MyBatis + MySQL 这套组合重新撸了一个完整版本。这篇文章把整个项目的设计思路、核心实现、部署步骤和典型踩坑都整理出来,想拿它做毕业设计或者进公司接二手项目的,可以直接按这个思路抄作业。

前后端分离这个事,本质上就是把“页面展示”和“数据处理”彻底拆开。前端只负责渲染界面和交互,后端只负责提供接口和数据。两者通过 JSON 通信,互不干扰。这样做的好处很明显:前端可以单独部署在 Nginx 上,后端可以打包成 Jar 独立运行,以后想换掉任何一端都不会把整个系统搞崩。房屋租赁这种业务,权限分三种角色,界面逻辑各不相同,分离架构反而比传统模板渲染更清晰。

1. 系统整体设计与技术选型思路

1.1 为什么选 SpringBoot + Vue 这套组合

做这个项目之前,我对比过几个方案:传统的 JSP + Servlet、SpringMVC + Thymeleaf、以及前后端分离的 Vue + SpringBoot。最后选了后者,不是因为它“新潮”,而是因为这套组合在不牺牲开发效率的情况下,解决了三个实际问题。

第一,线程安全和并发处理。房屋租赁系统的核心操作是房源抢看、合同签约、账单支付,这些操作集中在少数几张表上,如果后端没有清晰的接口隔离,很容易因为 Session 共享或者模板状态残留出 bug。前后端分离以后,后端接口天然是无状态的,配合 JWT 或者 Token 做登录态管理,每个请求都可以独立处理,部署时想做负载均衡也方便。

第二,团队协作和调试效率。Vue 的开发模式是组件化,一个页面由若干个组件拼装而成。比如房源列表页,可以分为搜索栏、房源卡片、分页器三个组件,每个组件单独调接口。后端同学只需要把接口定义好,前端同学用 Mock 数据就能并行开发。这一点在赶工期的时候特别重要。

第三,部署灵活。后端 SpringBoot 打成 Jar 包,扔到服务器上java -jar就可以跑。前端打包成静态文件,扔到 Nginx 里配个反向代理就完事。以后想加一台服务器做集群,或者把静态资源放到 CDN,都不需要改动业务代码。

1.2 项目目录结构与模块规划

一个清楚的项目结构,比代码本身更能体现工程素养。我的后端工程按职责分成了五个包:controller、service、mapper、entity、common。controller 只负责接收请求和返回结果,service 处理业务逻辑,mapper 是 MyBatis 的数据库访问层,entity 对应数据库表,common 放统一返回结果、异常处理、工具类。前端工程按视图和状态管理分成 views、components、router、store、api 五个目录。api 目录单独管理所有后端接口请求,每个模块一个文件,这样后端接口变动时,只需要改 api 目录里的对应文件。

数据库这边,我设计了七张核心表:用户表、角色表、房源表、租客表、合同表、账单表、操作日志表。用户表和角色表做权限控制,房源表存房屋基本信息和状态,租客表关联用户和房源,合同表记录签约期限和租金,账单表按月生成应收记录。操作日志表比较容易被忽略,但实际运营中特别有用,比如谁修改了房源价格、谁做了合同作废操作,都必须留下痕迹。

整个系统的接口设计遵循一个原则:资源导向。比如房源相关的接口,统一用/api/house前缀,往下细分/api/house/list、/api/house/add、/api/house/updateStatus。租客相关用/api/tenant。合同相关用/api/contract。这样的路径设计自解释性很强,前端调用时一眼就知道在操作什么资源。

2. 核心功能模块与数据库设计详解

2.1 三种角色的权限边界

房屋租赁系统的业务角色分三类:管理员、房东、租客。管理员负责审核房源、处理投诉、查看全局数据;房东负责发布房源、管理自己名下的合同和账单;租客可以浏览房源、发起签约申请、查看账单并支付。

权限控制我用了最简单实用的方案:一张用户表存username和password,一张角色表存角色编码,用户和角色多对多关联。后端在拦截器里校验 Token,再从 Token 中解析出用户 ID,查数据库获取角色列表。接口层面用自定义注解@RequireRole标注需要的角色,切面统一处理权限校验。比如管理员的“全局统计”接口标注@RequireRole("admin"),房东的“发布房源”接口标注@RequireRole("landlord")。这样做比在代码里到处写 if 判断要优雅得多。

前端也做了对应的路由守卫。Vue Router 的beforeEach钩子里判断当前用户角色,如果访问了没有权限的路由,直接重定向到 403 页面。这里要注意,前端的权限控制只能提升用户体验,真正的安全防线必须放在后端。因为任何人只要拿到接口地址,就可以绕过前端直接调用后端,所以后端接口的权限校验一点都不能省。

2.2 房源、合同、账单三张核心表的设计细节

房源表是最容易踩坑的一张表。除了传统的朝向、面积、楼层、租金,我额外加了status字段,用数字表示状态:0 未出租,1 已出租,2 审核中,3 已下架。为什么不用字符串?因为字符串“available”、“rented”在代码里飘来飘去特别容易写错,数字配合枚举类还能统一管理。还有一个容易忽略的字段是audit_time,管理员审核通过以后必须记录时间,方便后续统计审核效率。

合同表要设计成不可随意修改的类型。合同一旦签署,租期、租金、押金这些字段就不能再变了。我在实现时没有给合同表提供“修改”接口,只提供“作废”和“新建”接口。作废操作会把原合同的状态置为无效,并记录作废原因和操作人。这样数据链条是完整的,每一份合同的历史都能追踪到。

账单表建议按月生成,而不是等租客去查的时候再临时算。每个月 1 号,定时任务扫描所有状态为“生效中”的合同,自动生成当月的应收账单,账单初始状态是“待支付”。这样做的好处是,租客登录后直接就能看到这个月要交多少钱,房东也能在后台看到哪些租客还没交钱。生成账单的定时任务用 Spring 自带的@Scheduled注解就可以实现,不用引入额外的分布式任务框架。

2.3 数据库连接及 MyBatis 映射的注意事项

数据库连接池我用的 HikariCP,SpringBoot 2.x 之后默认就是它,性能好,配置简单。唯一要注意的是连接池的maxPoolSize,不过度设大,10 到 20 就够用。设太大反而会增加数据库的连接负担,系统高并发的时候容易把数据库拖垮。

MyBatis 的 Mapper 接口和 XML 映射文件命名必须要一致。我以前因为图省事,把 XML 文件放在另一个目录,结果启动报错找不到映射,折腾了半天。正确的做法是:在 application.yml 里配置mybatis.mapper-locations: classpath:mapper/*.xml,所有 XML 文件统一放在resources/mapper目录下,Mapper 接口全限定名必须和 XML 的 namespace 完全一致。另外一个容易踩坑的点是,查询结果映射到实体类时,数据库字段的下划线风格和 Java 的驼峰风格要开启自动转换,配置mybatis.configuration.map-underscore-to-camel-case: true,否则create_time映射不到createTime属性上。

3. 核心接口实现与前端交互流程

3.1 登录鉴权与 Token 机制

登录接口是整个系统的入口,也是安全性的关键。用户提交用户名密码后,后端先用 BCrypt 校验密码,成功则生成一个 Token,Token 里只存用户 ID 和过期时间,用 JWT 工具类签名。前端拿到 Token 后存在 localStorage 里,以后每次请求都在 header 里带上Authorization: Bearer <token>。

后端用一个拦截器统一处理 Token 校验,代码逻辑大致是这样:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new BusinessException("未登录或登录已过期"); } String token = authHeader.substring(7); // 解析 token,如果过期或签名异常,直接抛出异常 Long userId = JwtUtil.parseToken(token); request.setAttribute("userId", userId); return true; }

这里要说个细节:Token 过期时间不要设太长,4 到 8 小时比较合理。太久会有安全风险,太短则用户频繁掉线。如果项目需要“记住登录状态”,可以额外做一个 refreshToken 机制,但一定要有有效期限制,不能设计成永久的。

前端相应地封装了一个 axios 实例,在请求拦截器里添加 Token,在响应拦截器里统一处理 401 状态码,如果 Token 过期就清空本地存储并跳转到登录页。这样前后端各管一件事,边界清晰。

3.2 房源的发布、审核与上下架流程

房源发布功能是房东角色最常用的操作。房东在前端表单填写房源信息,包括标题、小区名、户型、面积、月租金、押金方式、房屋描述,上传多张图片。后端接口在接收时要注意,图片不能直接以 Base64 字符串写到数据库,否则数据库会暴涨。正确做法是把图片文件通过文件上传接口存到服务器指定目录,数据库里只存文件访问路径。Nginx 配置一个虚拟目录,把/static/upload映射到实际存放目录,前端直接拼 URL 就能访问图片。

房东提交房源后,房源状态变为“审核中”,管理员登录后台会看到待审核列表。管理员点击通过后,房源状态变为“未出租”,前端房源大厅就能看到。如果审核不通过,管理员填写驳回原因,房东端会收到提示。整个流程需要两个接口:一个是提交审核接口,一个是审核处理接口。审核处理接口里要加一个不变量:只有当前状态为“审核中”的房源才能被审核,否则流水线会错乱。这一点我用乐观锁机制解决,在更新语句里带上WHERE status = 2,如果更新行数为 0,说明状态已经被别人改了,直接返回操作失败。

3.3 账单支付流程的模拟实现

租金支付环节,如果接真实第三方支付渠道,需要商户资质和大量配置,学习项目里一般用模拟支付替代。我在账单模块中做了这样一个流程:租客点击“去支付”,后端生成一个支付单号,支付单号用时间戳加随机数生成,保证唯一。前端跳转到一个模拟收银台页面,租客选择支付方式,点击确认支付。前端调用“确认支付”接口,后端校验支付单号有效后,把账单状态从“待支付”改为“已支付”,同时记录支付时间和支付单号。

这里要注意,模拟支付不代表不用做幂等控制。也就是同一个支付单号不能支付两次。实现方法是,在确认支付接口里先查询账单状态,如果已经是已支付,直接返回“账单已完成支付”,不再做状态流转。同时在支付单表上给支付单号建唯一索引,双保险。

4. 本地开发环境搭建与启动配置

4.1 后端 SpringBoot 环境配置

先把必要工具装好:JDK 1.8 或 11、Maven 3.6 以上、MySQL 5.7 以上。我推荐用 JDK 8,因为 SpringBoot 2.x 对 JDK 8 的兼容性最好,部署环境也常见。创建数据库并导入源码里的house_lease.sql文件,这个文件里包含了建表语句和初始数据,管理员账号默认密码是 admin123,首次登录后建议立即修改。

application.yml 是项目里最核心的配置文件,重点检查三块内容:数据源配置、MyBatis 配置、JWT 配置。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/house_lease?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl jwt: secret: your-secret-key expire: 28800

数据源的 URL 里务必加上serverTimezone=Asia/Shanghai,否则 MySQL 8 连接时大概率报时区错误。这个坑我后来帮好几个同学排查过,基本都是清同一处配置。

启动项目以后,确保接口地址能通过浏览器直接访问。例如访问http://localhost:8080/api/house/list,如果返回 JSON 数据,说明后端已经正常工作了。

4.2 前端 Vue 环境配置与本地联调

前端需要 Node.js 12 以上,npm 或者 yarn 均可。进入frontend目录,先执行npm install安装依赖。安装过程如果特别慢,可以把 npm 镜像切换到国内源,执行npm config set registry https://registry.npmmirror.com。

启动开发环境的命令是npm run serve。启动之后,默认地址是http://localhost:8081。此时前端页面能打开,但接口请求还是指向后端的 8080 端口,需要配置前端代理。在项目根目录找到vue.config.js,配置 devServer 代理:

module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

配置完成后,前端发起的/api请求会自动转发到后端地址。这样可以绕开跨域问题,同时接口地址在代码里可以直接写相对路径,等部署到生产环境后也不需要改动前端代码。

4.3 通过 Postman 快速验证核心接口

后端接口调试我习惯用 Postman。先登录获取 Token,然后把 Token 放到 Authorization header 里,再调用需要鉴权的接口。下面的表格整理了需要重点验证的五个接口。

功能请求方式路径核心参数
用户登录POST/api/user/loginusername, password
房源列表GET/api/house/listpageNum, pageSize, keyword
发布房源POST/api/house/addtitle, area, price, description
审核房源POST/api/house/audithouseId, auditStatus
生成账单POST/api/contract/generateBillcontractId

验证登录接口时,重点看返回值里有没有 Token 字段。验证房源列表接口时,先不携带 Token,应该返回 401。再携带 Token 访问,应该返回房源数据。这两个接口验证通过,说明鉴权链路是通的。

5. 生产环境部署完整教程

5.1 后端打包与服务器运行

后端打包执行mvn clean package -DskipTests,打包完成后在target目录下会生成一个house-lease.jar文件。把这个 Jar 包上传到服务器,使用以下命令启动:

nohup java -jar house-lease.jar --spring.profiles.active=prod > app.log 2>&1 &

这里推荐把环境配置拆分成application-dev.yml和application-prod.yml。开发环境用本地的宽松配置,生产环境使用独立配置,数据库密码、JWT 密钥通过环境变量注入,而不是写死在文件里。

启动以后可以通过tail -f app.log查看日志,看到Started HouseLeaseApplication字样说明启动成功。检查后端端口是否监听,可以执行:

netstat -tlnp | grep 8080

如果端口没有监听,再去日志中定位异常原因,最常见的是数据库连接失败和端口被占用。

5.2 Nginx 部署前端与反向代理

前端打包执行npm run build,构建完成后会生成dist目录。把dist目录里的文件上传到服务器,比如放到/opt/nginx/html/rent目录下。然后在 Nginx 配置文件中增加一个 server 节点:

server { listen 80; server_name your-domain.com; location / { root /opt/nginx/html/rent; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; 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 是前端历史路由的回退配置,因为 Vue Router 默认使用 history 模式,刷新非首页路由时必须让 Nginx 把请求转到 index.html,否则会出现 404。第二段 location 是接口反向代理,前端所有请求都走/api前缀,Nginx 会转发给后端的 8080 端口。

配置完成后,执行nginx -t检查语法,再执行nginx -s reload重载配置。然后在浏览器访问公网 IP 或域名,验证页面和接口是否都正常。

5.3 数据库与文件存储的部署要点

生产环境的 MySQL 建议单独部署或者使用云数据库,不要跟应用放在同一台低配服务器上。数据库的字符集在初始化时就要设为 utf8mb4,这样中文和特殊符号都能正常存储。用户表的密码字段长度至少 60,因为 BCrypt 加密后的字符串固定是 60 位,长度不够会导致操作报错。

房源图片上传目录,建议放在应用目录之外的持久化路径下。比如/data/upload/house。因为应用升级时,我们通常会替换 Jar 包,放在 Jar 包同目录下的文件容易丢失。Nginx 增加一个静态映射:

location /upload/ { alias /data/upload/; }

同时在后端配置文件里设置上传根目录为/data/upload。这样图片地址可以干干净净地访问,不用动数据库里的路径。

6. 常见问题与排查心得

6.1 跨域问题的两种解决方式

开发环境下,前端地址是 8081,后端是 8080,浏览器会拦截跨域请求。我在开发联调时用的方案是前端 devServer 代理,也就是前面提到的 vue.config.js 配置。这种方案因为请求是同源发出的,浏览器完全感知不到跨域。

生产环境如果用 Nginx 反向代理,本质上也是同源代理,也不存在跨域。但如果前端静态文件和后端接口分别用了两个不同的域名,那就要在后端做全局 CORS 配置。

@Configuration public class CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }; } }

注意,allowCredentials(true)时,allowedOrigins不能是*,需要换成allowedOriginPatterns("*"),否则浏览器会拒绝携带凭证的跨域请求。

6.2 MyBatis 查询结果为空的排查思路

遇到过很多次接口返回null却没有报错的情况。排查思路按顺序来。先确认 SQL 在数据库客户端里执行有结果。再检查 Mapper 接口方法名和 XML 里的id是否完全一致,不一致根本不会走到查询逻辑。接着检查实体类字段名和数据库列名是否匹配,如果开启了驼峰转换,要确保配置写在application.yml中,而不是写在 SpringBoot 启动类上。最后检查返回类型,列表查询的resultType写实体类,单条查询的resultMap要确保id字段有映射。

还有一个很隐蔽的坑:如果实体类上有@Data注解,而查询结果集里多了一个数据库表中不存在的字段,MyBatis 会自动忽略,但前端序列化出来的 JSON 就会少字段。这种情况不好排查,最好的办法是严格保证实体类和表结构一一对应。

6.3 Vue 页面空白且控制台无报错

前端部署后如果出现页面白屏,通常是路由 mode 的配置问题。Vue Router 使用 history 模式时,Nginx 必须要配置try_files回退,否则刷新第二次就会 404,但首屏加载可能是正常的。

另一种情况是静态资源路径不对。打包后 index.html 中引用的 JS、CSS 路径是绝对路径/js/app.js,如果你的前端文件部署在服务器子目录下,根路径就找不到。解决方法是,在vue.config.js里设置publicPath: './',让打包后的资源引用变成相对路径。

我第一次部署时就犯了后一个错误,把 dist 文件放进 Nginx 的 html 根目录之后,页面打开是一片空白,F12 看到一堆资源 404。后来把 publicPath 改成./,重新打包,问题就解决了。

6.4 上传图片失败与磁盘权限问题

图片上传报错或者上传后无法访问,大多数情况不是业务代码的问题,而是服务器目录权限。Nginx 进程运行的账号通常是nginx或者www-data,后端 Java 进程的账号可能是root或其他。如果上传目录没有给对应账号写权限,就会报Permission denied。我习惯将上传目录的所有者设置成启动后端程序的用户,然后给目录755权限,同时不要禁用exec权限,因为后续可能会有人把可执行文件传上去,安全风险也要考虑。最好对上传目录做严格的扩展名白名单校验,不只限制图片类型,还要限制 MIME 类型和文件内容头部。

7. 优化建议与二次开发方向

基础系统做完能跑通以后,下一步做这些事情会让整个项目更有竞争力。

搜索这块,目前房源列表是简单关键字模糊搜索。可以引入更友好的筛选条件,比如地铁沿线筛选、租金范围滑块、朝向多选、面积区间。这些都是纯前端逻辑加上后端查询条件拼接,实现难度不大,但体验提升明显。

支付模块如果对接真实支付,需要后端增加支付回调接口。回调接口是第三方支付平台主动请求我们的服务器,所以不能依赖前端 Token,需要使用签名验证。真实回调的验签逻辑比较绕,但做毕业设计的时候,把模拟支付改成“伪支付回调”已经足够体现业务理解。

消息通知方面,可以增加一个站内消息模块。租客提交签约意向后,房东收到消息提醒;合同到期前 30 天,系统自动给租客发送提醒。这个模块可以复用操作日志表的设计思路,单独建一张消息表,字段包含接收人 ID、消息类型、内容、是否已读。

权限系统如果还想做细,可以在现有角色基础上增加数据范围。比如房东只能操作自己创建的房源,管理员可以查看全量数据。这个控制在后端查询时根据当前用户 ID 拼装条件即可,重点治“越权访问”的场景。

另外,前后端代码里目前存在少量重复代码,比如分页查询的封装、统一异常处理、前端时间的格式化工具。把这些抽成公共类以后,系统会更好维护。我一般会把分页响应封装成一个通用的PageResult类,所有列表接口统一返回{ total, records }结构,前端分页组件的对接成本会降到最低。

8. 我自己在实操中的几点体会

经过这个项目的完整开发,我个人的最大感受是,做好前后端分离项目的关键不在代码写得多花哨,而在于“接口契约先行”。开发前先花半天时间把接口文档的每个路径、请求参数、返回结构定义清楚,前后端各做各的,后面联调时几乎不会吵架。如果一边写一边改,状态管理、字段命名、错误码定义都会乱成一团,返工成本很高。

第二个体会是,日志比调试器好使。后端接口出问题时,第一步永远不是打断点,而是去看日志。我在每个关键接口加入参日志和出参日志,方便第一时间定位是前端传参问题还是后端逻辑问题。日志级别在生产环境设置为 INFO,在开发环境用 DEBUG,不用为了性能牺牲排查问题的便利性。

最后聊一聊方案落地。如果你是把这套代码作为毕业设计,答辩时重点讲清楚“为什么选择前后端分离”“角色权限怎么设计”“账单流程如何保证数据一致”这三个问题,比背十遍代码都管用。如果你是接野项目,顺利跑通之后一定记得做两件事:一是把数据库的自动备份配上,至少每天一次;二是把配置文件里的敏感信息用环境变量统一接管,别把数据库密码硬编码在代码里。因为等到项目真的上线,你会发现,稳定运行比炫技重要得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进