“上个月,把自己做过的一个Spring Boot + Vue房屋租赁系统整理了一遍,从需求分析、数据库设计,到前后端联调、问题排查,再到源码、数据库脚本、文档怎么交付,每个环节都重新踩了一遍。这个项目看起来不大,但涉及的业务流其实很完整:房源、租客、合同、账单、退租,一环扣一环。对在校学生、刚入行的开发者、小团队接单的人来说,都是一个很好的练手样本。”——这段话先放这儿,下面进入正题。
1. 项目整体拆解:一个小型租赁系统到底要管哪些事
1.1 需求边界:别把“小型”做成“大而全”
接手这个项目时,需求方给出的描述特别简短:能发布房源、管理租客、记录租约、生成账单,后台用Spring Boot,前端用Vue,交付源码、数据库和文档。这种描述我见过太多次,看着简单,实际上到处是坑。
我习惯先把业务边界画出来。一个小型房屋租赁系统,核心不是“管房子”,而是“管房子和钱的关系”。房源是基础数据,租客是参与者,租约是把房源和租客绑定起来的契约,账单则是租约落地后产生的资金流转记录。把这四条线理清楚,系统的主干就出来了。
我的做法是划分成五个模块:用户与权限、房源管理、租客管理、租约管理、账单与退租。用户与权限管谁能进系统、能干什么;房源管理管房屋的基础信息、状态和上下架;租客管理管租客的档案和历史记录;租约管理管签约、续租、变更和到期;账单与退租管押金、租金、水电费和房屋归还。
边界一旦定清楚,后面写代码和文档就顺畅很多。最怕的就是一开始想得很满,什么物业报修、投诉建议、在线支付统统要加。真加了,项目周期直接翻倍。小型系统的生存之道是“够用就好”,后续能扩展的设计比一次性堆功能更值钱。
1.2 目标用户和使用场景:不是所有租赁系统都需要复杂权限
这个系统的目标用户是三类:管理员、房东、租客。管理员通常是系统运营方,负责给房东开通账号、查看整体数据;房东是核心使用方,负责录入房源、签约、收租;租客在这个系统里通常只做浏览和查看自己的账单,不参与后台管理。
实际开发中,我做过多租户和单租户两种方案。如果是单小区、单房东使用,角色做成管理员和房东合并即可,省掉不少工作。如果是小型中介公司,每个房东只能看自己的房源和账单,那就要考虑数据权限隔离。
我建议直接做成三套角色的权限模型,后端用拦截器校验,前端用路由守卫控制页面访问。这样不管最终给谁用,改动成本都很低。一个常见的错误是只做登录校验,不做权限区分,结果普通租客能打开房东的账单管理页面,这属于上线前必须修的硬伤。
2. 技术选型与架构:Spring Boot + Vue 组合背后的理由
2.1 后端选型的三个理由:生态、效率、可维护性
后端用Spring Boot基本没有悬念。理由有三。一是生态成熟,无论是权限框架、数据库操作、文件上传,还是对接第三方支付,都有大量现成的方案可以查;二是开发效率高,Spring Boot的自动配置和约定优于配置,能让开发者把精力放在业务实现而不是重复搭建上;三是可维护性好,对任何一个Java开发者来说,接手一个Spring Boot项目都比接手一个自研框架容易得多。
具体实施时,我用了Spring Boot 2.7.18搭配MyBatis Plus 3.5.x。有人会问,为什么不用MyBatis?MyBatis Plus的查询构造器和分页插件能让CRUD代码量减少一半,特别适合这种以表单、表格为中心的业务系统。密码加密用BCrypt,登录状态用JWT,这些选型都是经过多个项目验证过的,不是随手的决定。
2.2 前端选型:Vue 2.7 + Element UI 的稳妥搭配
前端我用的是Vue 2.7加上Element UI。可能有同学觉得Vue 3都出那么久了,为什么还在用Vue 2?答案很简单:Element UI的组件生态最成熟,网上的资料最多,遇到问题最容易找到解决方案。对于以表格、表单、弹窗为主的管理后台,Vue 2.7 + Element UI是效率最高、上手最快的组合。
数据请求用axios封装,路由用vue-router,状态管理用Pinia或Vuex都行。我习惯在项目里用Pinia,因为它对TypeScript的支持更好,但如果是纯JavaScript项目,Vuex 3也完全够用。图表模块可以引入ECharts,用来做首页的房屋出租率、月度营收趋势等可视化展示,这个对系统观感提升特别明显。
2.3 数据库设计:五张核心表和它们的关联关系
数据库是整个系统的地基,设计得不好,后面业务再漂亮也是空中楼阁。我把核心表设计成五张:用户表、房源表、租客表、租约表、账单表,再辅助几张字典表,比如房源类型、合同状态。字段设计上,我有一条经验:通用字段宁可冗余,不要过度抽象。比如房源表里同时存了“所属房东ID”和“房东姓名”,看似冗余,实际在列表展示时能少写很多关联查询。
房源表的核心字段:编号、名称、地址、户型、面积、租金、押金、状态、房东ID、图片。租客表的核心字段:姓名、手机号、身份证号、紧急联系人、备注。租约表的核心字段:合同编号、房源ID、租客ID、起租日期、结束日期、月租金、押金、状态。账单表的核心字段:账单编号、租约ID、费用类型、金额、生成日期、状态(待缴/已缴/逾期)、支付方式。
主外键关系上,租约表是连接房源和租客的桥梁,账单表又挂在租约下面。这样设计的好处是,查询某套房子的历史账单时,只要通过租约ID一层关联即可,不必从房源直接跨到账单。状态字段统一用tinyint,0和1或0、1、2这种枚举值,不要用字符串,否则写SQL过滤时特别难受。
3. 核心功能实现:从登录鉴权到租约账单的关键细节
3.1 用户登录与JWT鉴权:不是把Token发出去就完事了
登录鉴权我每次都是重点讲。因为它是所有请求的入口,一旦这里设计不好,后面全是补丁。流程很简单:用户输入账号密码,后端校验通过后用userId、username和role生成JWT,设置过期时间,返回给前端;前端把Token存到localStorage,axios拦截器在每次请求前把Token塞进请求头。
后端配置一个拦截器,拦截除登录、验证码等白名单外的所有请求,从请求头里取Token,校验签名和过期时间,再放行。这个实现里最容易出问题的点是Token过期后的处理。我习惯的做法是:后端返回401错误码,前端路由守卫识别到401,清除本地Token并跳转登录页。这个流程在文档里必须写清楚,因为很多人做完登录后,Token过期时界面会白屏或者卡在空白页上。
密码存库用的是BCrypt加密,千万别用MD5加盐那套老办法。BCrypt每次生成的结果不同,校验时直接用matches方法判断,安全性和易用性都比自己写加盐好。数据库里密码字段长度记得设置成60以上,否则加密串会存不下。
3.2 房源管理:状态流转是设计重点
房源管理模块看起来就是简单的增删改查,但真正做得舒服的项目都会把“状态”设计好。房源的状态我划分成:未出租、已出租、已下架、维护中。房东发布房源时默认是未出租;签约成功后自动变为已出租;退租结算完成后变回未出租;房东手动下架或后台管理员下架时变为已下架。
这里有个细节:房源列表页的筛选条件,就需要针对“状态”做多选查询。我的实现方式是MyBatis Plus的QueryWrapper,当状态参数不为空时才拼接条件,避免用户不选状态时查不到数据。发布房源时还要处理图片上传,我把图片存储路径设置为可配置的上传目录,数据库只存相对路径(如/uploads/房源ID/图片名.jpg),这样部署后换服务器不用改数据库数据。
3.3 租约与账单:核心业务闭环
租约是业务闭环的中枢。签约时,系统要做两件事:插入租约记录、把房源状态改成已出租。退租时,要做三件事:把租约状态改成已结束、把房源状态改回未出租、结算最后一笔账单(包括水电费、违约金、押金退还)。
账单生成我建议做成“生成草稿,手动确认”的模式。为什么?因为自动生成账单虽然省事,但水电费这类数据经常要人工核对,一旦自动入账发现错误,退款流程会非常痛苦。我的做法是:签约时自动生成租期内每个月的租金账单草稿,月底由房东确认水电费后点击“确认账单”,账单状态变为待缴,再进入收款流程。
押金单独建一个账单类型,起租时生成押金账单,退租结算时生成一笔负数的押金退款账单,这样整个资金流水是完全对得上的,开发报表的时候不会被问“押金去哪儿了”。
3.4 租客和合租场景:别把简单问题复杂化
小型租赁系统,租客房东多为个人,不建议做一房多组的合租模型,否则账单分摊的复杂度会吃掉所有开发预算。我的建议是,一个房源在同一时间内只关联一条有效的租约,租客表里可以额外记录同住人信息,但不参与账单计算。
如果需求方明确只要有合租场景,我就会在租约表里增加“租客类型”字段,区分整租和单间,并在房源表里设置房间数量。但这属于二阶段需求,第一版不建议动。很多小型项目翻车,就是因为第一版就试图解决所有业务问题。
4. 实操过程:从源码到跑起来的完整步骤
4.1 环境准备:版本统一比版本最新更重要
运行这个项目,需要的环境如下:JDK 1.8或以上、Maven 3.6+、Node.js 14+、MySQL 5.7或8.0、Redis可选。我最常被问的问题是“能不能不加Redis”。如果JWT和登录状态都放在内存和数据库里,确实可以不加Redis。但在实际部署中,多实例场景下不用Redis会导致Session和Token校验出现麻烦,所以我建议还是加上,成本很低。
环境准备时最坑的一件事就是JDK版本和Spring Boot版本不匹配。Spring Boot 2.7在JDK 8上跑得最稳,不要用JDK 17去跑老版本依赖,否则遇到一堆反射相关的报错就会很痛苦。前端建议使用Node 14或16,不要一上来就装最新的Node 20+,因为部分老版本的node-sass装不上。
4.2 后端配置要点:application.yml里的五个必改项
拿到源码后,后端要改的核心配置都集中在application.yml里,千万别改代码。五个必改项是:数据源URL(数据库地址、库名、编码、时区)、数据库用户名密码、JWT密钥和过期时间、文件上传路径、端口号。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/rent_house?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你自己数据库的密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl jwt: secret: 自己生成一段足够长的随机字符串 expire: 86400000这里有个非常典型的坑:数据库字符串的characterEncoding必须设置,否则前端提交中文名存进数据库后全是问号。另一个坑是serverTimezone不加会导致时间字段差8小时。这两点在文档里一定要写清楚。
4.3 前端配置要点:接口代理和请求拦截
前端拿到源码后,要先看两个文件:vue.config.js(或vite.config.js)和src/utils/request.js。接口代理的作用是让前端开发环境可以通过配置,把/api开头的请求转发到后端8080端口。千万不要在代码里硬编码完整的后端地址,否则部署到服务器时改起来会想哭。
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };axios拦截器里要做三件事:请求头带上Token、响应返回时统一处理业务码、401时强制跳登录页。这部分写好后,前端每个接口调用就不需要关心Token和错误处理,组件里只处理业务逻辑就行。
4.4 初始化数据:管理员账号和演示数据怎么进库
SQL脚本里,除了建表语句,一定要加初始化数据。管理员账号建议用admin/admin123,用BCrypt加密后的字符串存到用户表。房源表里插入5套左右的演示房源,租客表插入几名演示租客,再插入几条状态不同的租约和账单。这样项目跑起来之后,首页图表、列表页、详情页都有数据可看。
我见过不少项目,SQL脚本里只有建表语句,没有初始数据。跑起来之后,首页一片空白,用户还要自己去数据库里一个个插入,体验特别差。一个高质量交付的数据库脚本,应该让使用者导入后直接登录后台,就能看到完整效果。
5. 实战还原的典型问题与排查技巧
5.1 跨域问题:配置了CORS还有错,多半是拦截器先拦了
前端访问后端时报跨域错误,是最常见的问题。排查思路是:先看后端有没有配置CORS过滤器,再看拦截器是不是放行了OPTIONS请求。很多人在拦截器里把OPTIONS请求也给拦截了,导致CORS预检直接失败,浏览器没法发起真实请求。我的解决方案是:写一个CorsConfig实现WebMvcConfigurer,然后再写一个JwtInterceptor,重写preHandle方法时先判断请求方式,若是OPTIONS就直接返回true放行。
5.2 日期字段差8小时和JSON序列化报错
日期差8小时有两类原因:一类是数据库连接URL里少了serverTimezone=Asia/Shanghai,另一类是Java对象里的日期格式没有指定时区。我在实体类的日期字段上直接加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),这样前端拿到的字符串时间就是北京时间。
JSON序列化的递归问题也经常遇到,典型表现是“StackOverflowError”或“Could not write JSON”。原因是实体类之间存在双向关联,比如租约里有房源对象,房源对象里又引用了租约集合。解决方法是:一处使用@JsonIgnoreProperties,或者在VO/DTO里只返回需要的字段。这个坑在小项目里防不胜防,我的建议是干脆不要懒加载,直接定义返回VO,把关联字段拼成扁平结构。
5.3 数据库连接失败:密码有特殊字符被URL拦截
MySQL密码里如果含有@、#、:等字符,直接放在连接URL里就会被解析成特殊字符,导致认证失败或无法连接。解决办法是对密码做URL编码。另一个更稳妥的做法是把密码单独放到环境变量或配置中心里,不写死在application.yml,但这对于小项目来说配置成本偏高,至少要在文档中给出编码说明。
5.4 分页查询返回空数据和总数对不上
MyBatis Plus的分页查询有个前提:必须配置分页插件PaginationInnerInterceptor,否则Page对象只能拿到当前页数据但查不出总数。还有一个常见问题是,有些人在SQL里同时写了Page参数和自定义的count查询,导致总数和列表数据使用了不同过滤条件,数字对不上。解决方法是检查所有查询条件是否使用同一个条件对象,或直接封装成统一查询DTO。
5.5 前端请求报404:路由漏配置或后端Controller路径不匹配
这类问题排查时先打开浏览器控制台,看请求的完整URL,再把前端接口文件和后端的Controller类做逐字对比。常见原因是Controller类上写了@RestController,但@RequestMapping的路径和前端不同。另一个坑是后端接口在权限拦截器的白名单外,返回的是401而不是真正的接口报错,这种情况看起来像是接口不存在,实际上是Token没带或过期了。
6. 交付物整理:源码、数据库和文档怎么给才算完整
6.1 源码目录怎么组织,别人接手才不迷路
源码部分,我的目录结构是固定的:backend和frontend两个一级目录,backend下按要求分为controller、service、mapper、entity、config几个子包,frontend下分为api、views、router、store、components几个子目录。每个模块命名统一用功能名来命名,比如HouseController、LeaseService。
文档里要画一份清晰的目录结构说明,把每个包的职责用一句话交代清楚。别小看这一点,很多项目交付后,接手的人花了一下午搞不清业务逻辑在哪个文件里,就是因为目录结构没有说明。我的经验是:这里多写两百字,后续能少接十个电话。
6.2 数据库脚本规范化:结构、数据、说明三者缺一不可
数据库交付我习惯放在一个sql目录下,拆成三个文件:schema.sql(建库建表)、data.sql(初始化数据)、说明文档(表结构说明和关系说明)。建表语句里要统一带IF NOT EXISTS,数据文件里要按插入顺序排好(先用户表,再房源表,再租约、租约、账单),否则外键关联会让使用者导入时直接报错。
数据库说明文档里一定要用表格列出每张表的核心字段、类型、含义。不要小看这个动作,它可以让使用者在没看后端代码的情况下直接理解业务逻辑。哪怕是“房源表里状态0代表未出租、1代表已出租、2代表下架”这种一句话,都能省掉后续大量沟通成本。
6.3 项目文档该写什么:按“傻瓜能跑起来”的标准写
项目文档的核心目标只有一个:让一个完全没接触过该项目的人,按照文档能把它跑起来。所以文档里至少要有:环境要求(具体版本)、导入步骤(把SQL脚本导入MySQL的命令或可视化操作步骤)、后端启动步骤(修改application.yml、执行mvn spring-boot:run)、前端启动步骤(npm install、npm run serve)、默认账号、常见问题排除(回到第5节列的那几个问题)。
文档里最好附上一份接口说明,列出主要接口的路径、请求方式、请求参数、返回示例。不要求像大厂那样弹出一份Swagger文档,但至少要把登录、房源增删改查、租约创建、账单确认这几个核心接口写清楚。还有一点是我踩过的坑:文档写好后,一定要自己按文档从头到尾跑一遍,你会发现有些步骤跳过了没记录,有些命令复制粘贴就会报错。这个自查过程能滤掉大量技术债务。
最后再分享一点个人体会
这个项目让我最受益的一次经验是,收到一个别人写的同类型项目,源码完整、数据库也有,但文档只写了三行字,我为了搞清楚一个“账单状态从待缴变成已缴是哪个接口触发的”,翻代码翻了两个小时。从那以后,我写交付文档的时候,一定会把“状态流转图”用文字加表格列清楚:哪个操作触发哪个状态变更,改完之后界面跳转到哪里。这件事对我自己后续的二次开发也特别有用。如果你正在做类似的Spring Boot + Vue项目,我强烈建议把状态流转写进文档里,而不是让它只存在于某个Service方法的if分支里。这套东西做完,项目变成培训素材、毕设、简历项目或接单案例,都能拿得出手。