做房屋租赁管理系统这类型项目我前后折腾过好几版。最早是单体结构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 映射的注意事项数据库连接池我用的 HikariCPSpringBoot 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 校验密码成功则生成一个 TokenToken 里只存用户 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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 里务必加上serverTimezoneAsia/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.activeprod app.log 21 这里推荐把环境配置拆分成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不用为了性能牺牲排查问题的便利性。最后聊一聊方案落地。如果你是把这套代码作为毕业设计答辩时重点讲清楚“为什么选择前后端分离”“角色权限怎么设计”“账单流程如何保证数据一致”这三个问题比背十遍代码都管用。如果你是接野项目顺利跑通之后一定记得做两件事一是把数据库的自动备份配上至少每天一次二是把配置文件里的敏感信息用环境变量统一接管别把数据库密码硬编码在代码里。因为等到项目真的上线你会发现稳定运行比炫技重要得多。