做企业级房屋租赁管理系统这套源码之前,我先被身边几个做租赁生意的朋友轮番"教育"过:房源几百套,租客合同散在文件夹里,收租全靠日历提醒,月底对账得拿Excel一个个拼。他们需要的不是那种绑定智能门锁的SaaS平台,而是能部署在自己服务器上、数据完全可控的管理系统。所以这套项目从立项起的目标就很明确——用SpringBoot+Vue+MyBatis+MySQL四件套,做一套能直接上生产环境的完整源码,开发人员拿到手能跑通、能改、能交付。
这套系统覆盖了租赁业务里最核心的"房、客、合同、钱"四条线:房东或运营公司可以用它管理房源信息、录入租客资料、生成租赁合同、登记收款退款、处理报修工单,管理层还能看到实时数据看板。技术架构是典型的前后端分离单体应用,不是微服务,但应付几百间房的租务管理绰绰有余。对刚学完Java后端和前端基础、想看完整企业级项目长什么样的开发者来说,这份源码最大的价值就是"能跑起来的完整项目",而不是教程里的零碎Demo。
下面我按自己当初从零搭建、踩坑、部署上线的过程来拆解这套系统,讲业务也讲实现,重点讲哪些地方容易出问题。
1. 这套租赁系统到底想解决什么问题
1.1 从租务管理的真实痛点说起
做租赁管理系统,最忌讳上来就画功能大饼。要先把业务痛点理清楚,系统才有存在的意义。我在做需求调研时整理了三大高频痛点,系统所有模块都是围着它们转的。
第一是"房源账实不符"。运营方手里的房源分布在好几个小区,每套房下面又有不同的房间,房间到底是空置、在租、还是维修中,靠表格登记很难实时更新。第二是"合同管理混乱"。纸质合同到期没人提醒,续签、退租全靠运气,押金退还扯皮多。第三是"对账效率低"。租金收没收、哪套房这个月还没交钱、某租客欠了几个月,这些数据散落在聊天记录和本子里,月底财务要花两三天才能理清。
系统的核心价值,就是把这三件事从"人肉管理"变成"系统状态机"。房源的每个房间都有状态字段,合同的起止日期通过数据库自动计算剩余天数,每一笔收款都关联到具体合同和房间,这样管理者打开工作台就能看到今日待办、即将到期合同、上月应收实收对比,而不是翻聊天记录。
1.2 功能模块全景:把"房-客-合同-钱"管起来
整个系统的功能模块可以拆成六大块,覆盖租赁运营的全流程:
- 系统管理:用户、角色、菜单权限,采用RBAC权限模型,管理员给不同员工分配不同操作权限。
- 房源管理:按照"小区/楼栋-房间"两级结构管理房源,房间支持新增、编辑、状态变更(空置、已租、维护)、条件搜索。
- 租客管理:登记租客姓名、身份证、手机号、紧急联系人等基础信息,并关联其历史租住记录。
- 合同管理:创建租赁合同,指定租客与房间,自动计算起止日期、租期、租金总额,支持退租、续租操作。
- 收退款管理:租金收款、押金收款、退款登记,每笔记录对应合同号,可按时间、房间、租客维度筛选。
- 报修与统计:租客报修工单的状态流转,数据看板展示房间出租率、月度应收实收、合同到期数量。
有的读者可能觉得这功能不算多,但做企业级系统本来就不是堆功能,而是把每个功能做扎实。比如房源管理里"房间状态变更"和合同"生效、退租"之间必须有联动逻辑——合同一旦生效,对应房间状态必须自动变成"已租",只有这样才能保证数据一致性。
1.3 技术选型为什么是这四件套
很多刚开始做项目的朋友总纠结用什么框架,我直接说结论:这套项目选SpringBoot+Vue+MyBatis+MySQL,不是追求新潮,而是为了稳。
SpringBoot负责后端接口和业务逻辑,自动配置机制极大减少了Spring的XML配置,内嵌Tomcat让项目部署变成"打一个Jar包"这么简单;Vue负责前端页面交互,配合Element UI组件库,开发后台管理界面的效率非常高;MyBatis负责数据库访问,骨灰级SQL控会更喜欢直接写SQL的感觉;MySQL存储业务数据,开源、稳定、维护成本低,中小型租赁企业的数据量完全够用。
我见过不少团队在这种项目上一上来就引入微服务、Redis、MQ,结果租务系统本身业务并不复杂,分布式架构反而带来部署和运维的复杂度。做企业级项目,技术选型的第一原则是"够用且可控",而不是"越高级越好"。这套四件套组合,任何一个Java开发都能快速接手。
2. 环境准备与源码目录:先把项目跑起来
2.1 版本组合表:照着装,别用最新版
这是我踩过最深的坑,所以必须放到最前面说。很多初学者喜欢装最新版环境,结果发现SpringBoot版本太高导致各种兼容问题。比如SpringBoot 3.x要求JDK 17及以上,把javax.servlet包改名成jakarta.servlet,老项目里的拦截器、过滤器代码直接迁移不过去。所以这套源码我推荐使用下面这套"保守但经过验证"的版本组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 企业生产环境主力版本,兼容性最好 |
| Maven | 3.6.3 | 与JDK 1.8搭配稳定 |
| SpringBoot | 2.7.x | 基于JDK 1.8的最后几个大版本之一 |
| MyBatis Starter | 2.2.x | 配合SpringBoot 2.7使用,自动配置正常 |
| Node.js | 14.x或16.x | 适配Vue 2和旧版构建工具 |
| Vue | 2.6.x + Element UI 2.15 | 后台管理系统经典组合 |
| MySQL | 5.7.x | 稳定性好,生产案例多 |
注意:如果你执意用SpringBoot 3.x,需要同步升级到JDK 17,且很多第三方starter可能不支持,排查起来非常折腾。我个人建议先按推荐版本来,等系统跑通后再考虑升级。
2.2 源码目录结构与前后端分离的设计
这套源码分为backend(后端)和frontend(前端)两个独立工程,这是前后端分离的标准姿势。你可以先各自启动,通过代理联调,最后也可以把前端打包产物放到后端一起启动,两种方式我都验证过。
后端目录结构大致是这样:
com.rent.system ├── controller // 接口层,接收前端请求 ├── service // 业务逻辑层,处理具体规则 ├── mapper // MyBatis的Mapper接口 ├── entity // 实体类,对应数据库表 ├── config // 配置类,如跨域、拦截器、静态资源映射 ├── common // 通用返回结果、异常处理、工具类 └── resources ├── mapper // MyBatis XML映射文件 └── application.yml前端目录是标准Vue CLI工程:
src ├── api // 封装axios请求,按模块分文件 ├── router // 路由配置,含动态路由逻辑 ├── store // Vuex状态管理,存用户信息、权限 ├── views // 页面组件,按模块目录组织 ├── components // 公共组件 └── utils // request封装、工具函数这套目录的精华在于"按模块分包",而不是"按技术类型分包"。controller/service/mapper各自对应一个业务模块,比如合同相关的代码都放在contract包路径下,新人接手代码时找起来非常快。这也是企业级项目里比较推荐的做法。
2.3 数据库初始化与配置文件改动点
数据库脚本放在sql/rent.sql目录下,里面包括建库建表语句和初始数据。导入方式我建议用命令行而不是图形化工具,命令很简单:
mysql -uroot -p < rent.sql导入完成后,里面默认有一个管理员账号:admin / admin123,密码在数据库里是BCrypt加密存储的,你别直接往数据库里改明文,要改密码就走系统里的"修改密码"功能。
真正需要你改动的配置不多,在后端application.yml里主要改三处:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/rent?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码这里有个热词里很多人问的"mysql ssl连接错误",就是useSSL参数没设置。MySQL 5.7默认会尝试SSL连接,如果不显式设为false,本地开发环境经常报SSL connection error或警告信息。另外字符集必须指定utf8,否则中文字段乱码会折腾你半天。
前端配置在vue.config.js里的devServer代理,开发模式下前端跑8081端口,通过代理把/api开头的请求转发到后端8080端口,这样就不会有跨域问题。默认配置已经写好了,你只需要确认后端端口一致即可。
3. 核心业务模块是怎么一步步落地的
3.1 登录认证与RBAC权限:动态路由的实现
企业级系统第一个要解决的就是权限问题,不然谁都能进后台、谁都能删数据。这套系统的权限模型是经典的RBAC(基于角色的访问控制),数据库里对应五张表:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。
登录流程是这样:用户提交用户名密码,后端用BCrypt校验密码,通过后生成一个JWT令牌返回给前端。前端把JWT存在localStorage里,每次请求在axios拦截器里带上Authorization头。后端用一个拦截器统一校验,如果令牌过期或非法,直接返回401,前端跳回登录页。
权限的精细化体现在菜单上。用户登录成功后,后端会根据用户角色返回对应菜单列表,前端拿到菜单后动态添加路由,也就是热词里提到的"Vue动态路由"场景。这样做的好处是:普通员工登录后根本看不到"用户管理"这个菜单,就算他在浏览器里手动输入管理页面的URL,路由没有被注册,也会被拦下来。这种"菜单隐藏+路由拦截"双保险,才是企业系统该有的姿态。
我之前单独写过一篇动态路由的实现细节,这里给个核心思路:
// router/index.js const fixedRoutes = [...]; // 登录页、首页等固定路由 const dynamicRoutes = []; // 需要权限才能访问的路由 router.addRoutes(dynamicRoutes); // 登录后根据权限动态添加这里有个小坑:如果用户退出登录后没有重置路由,下次换个账号登录,前一个账号的动态路由还在内存里。别忘了在退出时window.location.reload()刷新页面,把路由恢复初始状态。
3.2 房源管理:多条件分页查询的动态SQL
房源管理模块看起来就是一张表的增删改查,但企业级系统的增删改查和课程Demo有个明显区别:查询条件多且分页。用户可能按小区名查、按房间状态查、按户型查、按租金区间查,条件可以任意组合,这就要靠MyBatis的动态SQL来解决了。
先看页面字段:房源列表的搜索栏里有关键词输入(匹配房源名称或小区地址)、房间状态下拉框(空置/在租/维护)、租金范围两个输入框。对应的Mapper XML里是这个样子:
<select id="pageList" resultType="com.rent.system.entity.Room"> SELECT r.*, h.house_name AS houseName FROM t_room r LEFT JOIN t_house h ON r.house_id = h.id <where> <if test="keyword != null and keyword != ''"> AND (h.house_name LIKE CONCAT('%', #{keyword}, '%') OR r.room_no LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="status != null and status != ''"> AND r.status = #{status} </if> <if test="minRent != null"> AND r.rent >= #{minRent} </if> <if test="maxRent != null"> AND r.rent <= #{maxRent} </if> </where> ORDER BY r.create_time DESC LIMIT #{offset}, #{pageSize} </select>你可能注意到我写模糊查询用的是CONCAT('%', #{keyword}, '%'),而不是'%${keyword}%'。后者写法更短,但${}是字符串拼接,存在SQL注入风险,而且遇到特殊字符容易出问题。用CONCAT函数配合#{keyword}预编译,才是规范做法。我见过不少项目图省事,这里必须纠正过来。
还有一个细节是<=。在XML文件里,<符号会被当成标签开始解析,所以小于号必须写成<。这个错误很隐蔽,写XML时一不留神就会掉坑里,报错信息往往是"元素内容必须由格式正确的字符数据或标记组成"。记住:在MyBatis XML中,>可以正常写,<必须转义,或者用<![CDATA[ <= ]]>包起来。
3.3 租赁合同:时间计算与到期提醒
合同模块是整个系统的业务核心。一张合同要关联三个对象:租客、房间、收款记录。创建合同的页面里,运营人员选择租客(从已登记的租客里选)、选择房间(只看状态为"空置"的房间)、填写起租日期和结束日期,系统自动计算出总租金。
有几个关键的业务逻辑我得展开说。
合同编号的生成规则,我是这样设计的:HT + 年月日 + 序号,比如HT20250112001。这个编号在数据库里建立唯一索引,后续所有收款记录、退款记录都拿合同编号做关联,方便追溯。
租金计算不能简单写成"月租金乘以月数",因为合同可能不是整月,比如1月15日入住、租到3月10日。这套系统里我封装了一个租金计算工具类,按实际天数计算月租金,并支持押一付一、押一付三、半年付、年付四种付款方式。具体实现上,是先把起止日期拆分为整月部分和零散天数部分,整月按约定月租计算,零散天数按月租金除以当月天数乘积。
到期提醒的做法,是定时任务+SQL组合。后端用Spring的@Scheduled注解,每天凌晨跑一次任务,扫描所有状态为"生效"的合同,查出结束日期在30天内到期的合同,生成提醒记录写入提醒表。前端首页的"待办事项"就是从提醒表里查出来的:
<select id="getExpiringContracts" resultType="Contract"> SELECT * FROM t_contract WHERE status = 1 AND end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY) </select>合同状态流转也要把好关:创建后是"待生效",收款押金后变为"生效中",到期自动变为"已到期",运营人员操作退租变为"已退租"。每次状态变更都往合同操作日志表写一条记录,这样后面万一出现纠纷,能查到完整的操作轨迹。
3.4 收退款记录:账目怎么跟合同联动
收退款模块负责处理钱,逻辑必须严谨。系统里的每一笔收入都关联到具体合同,页面展示的字段包括:合同编号、房间号、租客姓名、收费项目(房租/押金/其他)、金额、收款方式、经办人、收款时间。
设计上有个关键点:收退款表和合同表是"多对一"关系,而不是简单在合同表里加一个"已收金额"字段。为什么要这样?因为合同可能会多次收款,比如半年付的合同分两次收,如果只在合同表里存累计金额,那收一笔改一次,并发操作下很容易数据错误。拆成独立的支付记录表后,合同总收金额由SQL聚合查询得出:
SELECT contract_id, SUM(amount) FROM t_payment WHERE contract_id = 1 GROUP BY contract_id;退款逻辑反着来,退款金额必须校验不能超过该合同已收金额,否则系统直接拒绝。这也是为了数据安全——未收款先退款这种事绝对不能发生。
财务人员最头疼的月度对账,系统里有一个汇总统计接口,查询某段时间内每天的应收、实收、欠费金额。实现上就是按合同关联支付记录,再按日期做聚合。这个接口的SQL写得稍复杂,但其实就是把业务规则翻译成SQL的过程,一旦写好了,月底财务报表几分钟就能出来。
4. 前后端联调中我实测过的那些坑
4.1 跨域问题:开发代理与CORS要一起处理
前后端分离联调,第一个遇到的就是跨域。前端在8081端口,后端在8080端口,浏览器直接请求肯定报跨域错误。我的处理方式是"前端代理为主,后端CORS兜底"。
前端在vue.config.js里配置代理:
devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这样前端发起的/api/login请求会被代理转发到后端,浏览器看起来是同源请求,跨域问题就消失了。
后端同时配置CORS,是为了防止万一前端不走代理直接请求后端IP的情况,比如后续有人用Postman测试接口、或者前端独立部署时直连后端。配置CORS不复杂,一个配置类搞定,但有几个参数要注意:允许的域名别用*,改成前端实际部署地址;allowedHeaders要包含Authorization,否则前端带JWT请求会被拒绝。
经验之谈:生产环境如果前后端分开部署,Nginx反向代理是最干净的方案,后端的CORS配置可以保持宽松,Nginx层面去控制跨域;如果前后端打包在一起,CORS配置基本都用不上,因为同源了。
4.2 MyBatis XML里的三个隐蔽错误
这段时间我帮不少人看过这套系统的报错,发现问题几乎都出在MyBatis的XML映射文件里。这里把最常见的三个坑集中说一下。
第一个是#{}和${}混用。分页查询里,LIMIT后面的offset和pageSize我见过有人写成#{offset},大部分时候没问题,但如果数据库驱动不支持预编译设置参数,就会报错。更隐蔽的是ORDER BY排序字段,如果把列名写成#{sortField},生成的SQL会是ORDER BY 'create_time',字符串当列名用,查询结果完全不对。所以记住:排序字段、表名这类不能参数化的地方用${},但必须自己做好白名单校验;值类型条件一律用#{}。
第二个是XML转义问题,我前面提到过<。还有动态SQL里如果<if>判断的字符串比较,写着写着就漏掉转义,编译不报错,运行到那一段才报"XML解析异常",排查起来很折磨。
第三个是主键回填。插入合同记录后,后面的退款记录要引用合同ID,如果Mapper没有配置useGeneratedKeys,你拿到的实体对象ID永远是null,接着往下做事必报错或者存出脏数据。规范写法是在插入语句上加:
<insert id="insertContract" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_contract ... </insert>4.3 Vue路由刷新404与打包部署问题
热词里"vue打包放进springboot中"这个需求我太熟了。Vue默认用history模式路由,路径长得像http://localhost:8080/contract/list,这种路径在Vue开发服务器里没问题,但如果你把前端打包后的dist目录直接丢进SpringBoot的static文件夹,刷新/contract/list页面就会404——因为后端Tomcat压根没有这个路径的映射,它只会找/index.html。
解决办法有两个。一是前端路由改用hash模式,URL变成http://localhost:8080/#/contract/list,刷新时不会请求真实路径。这个方案简单但URL不好看。二是保持history模式,在后端加一个转发规则:所有不存在的非静态资源路径,都转发到/index.html。推荐第二种,方案如下:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{spring:[^\\.]*}").setViewName("forward:/index.html"); } }这里面[^\\.]*的意思是不含点号的路径才会转发,这样图片、JS、CSS等静态资源不会受影响,只有类似/contract/list这样的前端路由会被转发到index.html,然后由Vue路由接管渲染。
4.4 图片上传与文件存储的取舍
系统里房源管理支持上传房间照片、户型图片,租客管理支持上传身份证照片。图片存储是个容易被低估的问题。
我最初的实现是把图片保存到本地磁盘,数据库只存相对路径,然后通过SpringBoot的静态资源映射暴露出去。配置很简单:
spring: resources: static-locations: file:D:/upload/但部署到服务器后问题来了:如果服务器磁盘满了,或者项目重启导致上传目录丢失,图片就全挂了。而且如果以后要做多实例部署,本地存储的图片在另一台机器上根本访问不到。所以我的建议是:小规模部署用本地存储没问题,但数据目录一定要和项目目录分开,并做定期备份;如果客户预算允许,直接上对象存储(如阿里云OSS、腾讯云COS),上传成功后把URL存进数据库,永久有效且不占服务器空间。
另一个和图片相关的小功能是图片预览。Vue里展示图片URL,如果图片路径是后端相对地址,必须在前端拼接基础路径。我在axios的request封装里统一处理了:响应头里的图片URL自动补全为完整地址,这样页面里直接<el-image :src="row.photoUrl">就能显示,不需要每个页面单独处理。
5. 打包部署的两种方式与后续扩展思路
5.1 方式一:前端打包直接放进SpringBoot
这种方式适合部署简单、一台服务器搞定的场景,也是热词里"vue打包放进springboot中"的完整答案。
第一步,前端构建。在frontend目录下执行:
npm install npm run build构建完成后,dist目录里是静态文件。第二步,把dist下的所有文件复制到后端src/main/resources/static目录里。第三步,重新打包后端:
mvn clean package -DskipTests得到的Jar包就自带了前端页面。启动时访问http://服务器IP:8080,直接看到登录页面,前后端同源,没有跨域问题。这种方式最大的优点就是部署简单,一个Jar包搞定,特别适合客户那边没有运维人员、只能用java -jar命令启动的场景。缺点是前端每次更新都要重新打后端包,迭代频繁时会麻烦一些。
5.2 方式二:Nginx反向代理独立部署
如果项目后期前端更新频率高,或者需要单独给前端配置HTTPS证书,那就走前后端独立部署。
前端构建产物dist上传到服务器的/usr/share/nginx/rent-web目录,后端Jar包正常运行在8080端口。Nginx配置如下:
server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /usr/share/nginx/rent-web; index index.html; try_files $uri $uri/ /index.html; # 解决history模式刷新404 } # 后端接口 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里面的try_files $uri $uri/ /index.html是Nginx版的"路由兜底",原理和SpringBoot转发一致:找不到真实文件的路径,就交给前端路由去处理。这套方案的前端更新只需要替换dist目录文件,然后nginx -s reload,后端可以完全不动。
5.3 这套系统可以往哪个方向继续扩展
项目做完交付之后,我一直在思考它的边界在哪里。按企业级房屋租赁管理系统的定位,目前这套源码算是个扎实的基础版,但有几个扩展方向,建议有需要的读者根据自己的业务场景去加。
第一是消息提醒的多元化。目前到期提醒只在系统内部展示,接上短信或微信模板消息后,运营人员不用天天登录后台就能收到提醒。实现上可以在定时任务里增加调用短信平台的HTTP接口的代码,工作量不大,但感知提升明显。
第二是电子合同。现在的系统还停留在"管理合同信息"的层面,如果业务正规化,需要接入电子签章服务,把合同生成PDF、在线签署、自动归档。前端展示PDF这块已经有很多现成方案,前端预览合同打印件是很自然的延伸。
第三是数据报表更精细化。目前的数据看板展示基础指标,后续可以增加租客流失分析、房源空置周期统计、同户型租金趋势分析。这些都是基于现有表数据就能实现的统计需求,SQL写好后前端用折线图、柱状图展示即可。
从我个人的实操体会来说,做完一套完整的企业级项目,收获最大的不是某个框架的API怎么调,而是建立了一种"从业务需求倒推技术实现"的思维方式。比如合同到期提醒,先想清楚谁能受益、什么时候触发、提醒内容是什么,再考虑定时任务怎么写、SQL怎么查;比如权限控制,先分析角色有哪些、菜单怎么分,再去实现动态路由。这套源码的每一行代码背后都有对应的业务决策,你把业务逻辑理顺了,技术实现自然水到渠成。最后再分享一个建议:拿到源码之后别急着满项目找彩蛋,建议从启动日志开始看,沿着一个"创建合同-关联房间-收款-看统计"的完整流程走一遍,比只看单个模块的理解效果好得多。