ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot+Vue房屋租赁系统:从数据库设计到部署实战

Spring Boot+Vue房屋租赁系统:从数据库设计到部署实战 1. 项目整体设计与技术选型这套基于Spring Boot Vue的小型房屋租赁系统是我整理过的最适合拿来直接改的业务型项目之一。源码、数据库脚本、部署文档三件套齐全下载后按文档把MySQL建库、改一下连接配置、前端装依赖基本半小时就能跑起来。它不追求技术上的花活核心是把房东平时最头疼的房源信息、租客档案、合同账单、退租结算这些事变成一套能操作、能统计、能留痕的后台。适合参考的人群有两大类一类是正在做毕业设计或者课程设计的同学Spring Boot Vue这套组合本身就是目前国内Java后端项目的主流搭配拿它做基础再扩展一个模块工作量可控另一类是确实有小型租赁管理需求的个人或团队比如手上有几十套房子的二房东或者做公寓短租运营的小团队这套系统的表结构和页面逻辑是可以直接沿用的。1.1 为什么选Spring Boot Vue这个组合先聊技术选型。房屋租赁系统属于典型的管理信息系统核心操作就是增删改查加状态流转没有高并发、没有复杂分布式事务所以选型的第一原则是“稳定、好招人、资料多”。Spring Boot自带的自动配置和起步依赖让后端开发不用处理一堆XML配置Vue做前端SPA页面交互比传统JSP模板流畅得多而且组件化之后房源卡片、状态标签、表格分页这些UI块都可以复用。从生态角度看Spring Boot Vue是国内中小型项目里被验证过无数次的搭配。遇到问题随便一搜不管是数据库连接池配置、接口跨域还是打包部署都有大量现成经验。相比之下如果引入微服务、消息队列、分布式缓存这些中间件对这个体量的项目反而是负担部署环境要求高、出问题的排查链路长、学习成本全部转嫁给后续维护的人。这个项目还额外做了集成MinIO做文件存储的设计原因是房源图片、合同扫描件这类非结构化数据不适合直接塞进MySQL的Text字段单独搞一台MinIO既能统一管理附件又能在不依赖第三方云存储的情况下做到私有化部署。MinIO是开源的对象存储服务兼容S3协议公司内网一台2核4G的小机器就能跑得很稳。1.2 功能模块拆解与业务边界房屋租赁系统的业务边界要清楚不能什么功能都往里塞。这套项目一共拆成六个核心模块模块核心功能对应的主要表房源管理房源录入、上下架、状态变更、图片上传house_info租客管理租客档案、证件信息、联系方式维护tenant_info合同管理租赁合同创建、续签、退租登记lease_contract账单管理水电费账单生成、收付款记录、欠费统计bill_info、payment_record报修管理租客报修、指派处理、处理结果回填repair_order系统管理用户、角色、菜单权限、操作日志sys_user、sys_role、sys_menu每个模块的边界划分是有讲究的。比如“合同管理”和“账单管理”之所以拆成两张主表是因为合同是长期存在的租赁关系而账单是按周期生成的流水租客可能签一年合同但是每个月都会产生水电账单和房租账单如果混在一个表里统计维度会非常难写。实际做的时候还要区分“应收账单”和“实收记录”这条后面讲表结构时会具体说。1.3 项目结构设计原则这个项目的目录结构遵循了后端最经典的分层controller层只做参数接收和结果返回service层写业务逻辑mapper层用MyBatis-Plus操作数据库entity对应表结构dto负责接口出入参。前端按views、components、router、api、utils拆目录。这样拆的好处是每一个类或者文件都有明确的“责任范围”出问题时能按层排查。另一个设计原则是“能用简单方案就不用复杂方案”。比如权限模型用的就是经典RBAC用户关联角色、角色关联菜单没有引入Spring Security的复杂配置而是用拦截器加JWT做登录态校验菜单权限通过查询用户角色对应的菜单列表返回给前端动态生成路由。这样做对小型管理系统完全够用而且代码容易读懂。后面二次开发如果想加“数据权限”只需在查询SQL里加上org_id过滤条件不必改动整体架构。2. 核心功能与数据库设计数据库设计是这个项目的灵魂也是我反复提醒自己不要偷懒的部分。很多同学做系统喜欢先写页面再补表结果页面改三轮表结构就要跟着推倒两次。正确顺序应该是先把业务实体和关系理清楚再建表再写接口最后做页面。2.1 用户与角色权限模型用户权限这块用的是五张标准RBAC表sys_user、sys_role、sys_user_role、sys_menu、sys_role_menu。sys_user存用户名、密码、手机号、状态sys_menu存菜单ID、父级ID、菜单名称、路由地址、组件路径、排序。菜单是树形结构页面左侧导航栏就是根据当前用户能访问的菜单列表动态渲染出来的。密码存的是BCrypt加密串不是明文也不是简单的MD5。BCrypt每次加密同一个密码得到的密文都不同但是校验时又能正确匹配这种设计能有效对抗彩虹表攻击。建表时密码字段长度要留够60位我见过有人把password设计成varchar(32)结果BCrypt密文存不进去启动一查数据全是截断的。用户表结构大致如下这种写法在小型项目里很常见CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt密文, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, status tinyint(4) DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;2.2 房源、合同与账单表结构房源表house_info是整套系统的核心主数据。每个房源除了基础字段还必须有状态字段比如0空置、1已预定、2已出租、3维修中。为什么要单独存状态而不是每次通过查询合同来判断因为“这套房子现在能不能租”是列表页最高频的查询条件如果每次都要关联合同表并且判断时间区间SQL性能会很难看而且逻辑容易出Bug。合同表lease_contract记录的是租赁关系需要注意几个容易被忽略的字段起租日期start_date、结束日期end_date、押金deposit、租金rent、付款周期pay_period、状态status。合同状态和房源状态要联动合同一旦生效对应房源状态就要变成已出租合同退租流程走完后房源才能回到空置。账单表我强烈建议拆成两块bill_info存应收账单payment_record存实收流水。一个账单可能被分多次支付比如租客先付了一半剩下一半月底再补如果把实收金额直接覆盖在bill_info上历史流水就丢了。正确做法是bill_info只维护“应收金额”和“已收金额”两个汇总字段每一笔实收都插入payment_record再通过update语句把已收金额累加。房源表的核心字段示意如下CREATE TABLE house_info ( id bigint(20) NOT NULL AUTO_INCREMENT, house_code varchar(20) DEFAULT NULL COMMENT 房源编号业务上可读, title varchar(100) NOT NULL COMMENT 房源标题, address varchar(200) NOT NULL COMMENT 详细地址, area decimal(10,2) DEFAULT NULL COMMENT 面积(平米), floor varchar(20) DEFAULT NULL COMMENT 所在楼层, rent decimal(10,2) NOT NULL COMMENT 月租金, deposit decimal(10,2) DEFAULT NULL COMMENT 押金, status tinyint(4) DEFAULT 0 COMMENT 0空置 1预定 2已租 3维修, landlord_id bigint(20) DEFAULT NULL COMMENT 房东/业主ID, cover_image varchar(255) DEFAULT NULL COMMENT 封面图路径, description text COMMENT 房源描述, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_status (status), KEY idx_address (address) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源信息表;2.3 数据库表关系与关键约束表之间的关系其实不复杂一个房源可以有多条合同记录但同一时间段内只能有一条有效合同一个租客可能在多个房源下有过租房记录所以就不适合把租客ID直接写在房源表上而是通过合同表建立“房源-租客”的多对多关系。这个设计是很多新手容易犯错的地方我见过有人直接在house_info表里放tenant_id结果一个房源换过三任租客后就只能覆盖旧记录历史完全没有。关键约束方面至少要注意三件事所有金额字段用decimal绝不用float或double否则到分以后会出现精度误差。时间字段统一用datetime合同开始结束时间要加索引因为报表统计经常按时间范围过滤。手机号、身份证号这类字段要建立唯一索引或者至少在应用层做重复校验避免同一租客重复建档。外键在这个项目里不强制物理外键。逻辑上通过service层保证引用完整性物理上只建普通索引。原因是在实际业务中如果直接在表上建级联外键删除顶层数据时容易误伤而且后续做分库分表时外键会成为阻碍。只要代码里事务控制得当物理外键在中小项目里省掉是合理的取舍。3. 后端实现与接口设计后端没有太多高深的技术但每个环节都有值得注意的细节。这一节我从工程搭建、业务状态流转、接口规范、文件上传四个方面讲。3.1 Spring Boot工程搭建与分层结构工程使用的是Spring Boot 2.7.x版本。为什么不直接用最新的Spring Boot 3.x因为3.x基于Jakarta EE很多老版本第三方依赖不兼容特别是MinIO客户端、MyBatis-Plus旧版本、某些swagger增强包很容易出现编译时找不到javax类的问题。对业务系统来说稳定压倒一切没必要为了追新而给自己埋坑。pom文件里核心依赖就这些dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyapplication.yml里的数据源配置是这样的spring: datasource: url: jdbc:mysql://localhost:3306/house_rental?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 30MB分层方面我习惯按功能模块建包而不是按技术类型建包。也就是说包名类似com.example.house.rent/house、/tenant、/contract、/bill每个包下面再放controller、service、mapper、entity。这样改一个业务功能时所有相关文件都在同一个包路径下不用翻来翻去找。团队协作时每个人负责一个包合并代码的冲突也少。3.2 核心业务逻辑租房状态流转租房系统里最容易出Bug的就是状态流转。如果不做约束任何状态之间都能随意切换就会出现“房子还在租着又被预订给了别人”这种低级问题。我在这套项目里用了状态机的方式处理。房源状态共有四个允许的流转路径如下当前状态允许流转到触发操作空置已预订、维修中租客看房后交定金预订或房东报修已预订空置、已出租预订取消或签订合同已出租退租中、维修中租客发起退租或租期内维修退租中空置退租验收通过房源重新释放后端实现时不在service里散落一堆if判断而是封装一个HouseStatusHandler所有状态变更都走同一个入口。核心代码如下public void changeStatus(Long houseId, Integer targetStatus) { HouseInfo house houseInfoMapper.selectById(houseId); // 此处从状态机配置中校验当前状态是否允许跳到目标状态 if (!statusMachine.canTransit(house.getStatus(), targetStatus)) { throw new BizException(当前房源状态不允许执行此操作); } house.setStatus(targetStatus); houseInfoMapper.updateById(house); }这个方法加上Transactional事务后就能保证“改房源状态”和“写合同记录”这两个操作要么一起成功要么一起回滚。我在最开始做的时候忘记加事务结果出现过合同建好了房源状态没改过来前台列表显示空置又被第二个人签了合同的严重脏数据。3.3 接口规范与统一返回体接口设计必须统一返回结构否则前端写axios拦截器时会疯掉。这套项目所有接口返回的都是Result对象格式如下{ code: 200, message: 操作成功, data: { } }code为200表示成功400表示参数错误401表示未登录或登录过期500表示服务器异常。前端axios响应拦截器里只需判断code不需要为每个接口单独处理错误分支。统一返回体对应的Java类也很简单Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }全局异常处理器用RestControllerAdvice捕获业务异常和系统异常避免把一堆堆栈信息直接抛给前端。这里要提醒一下不要把系统异常信息直接拼在Result的message里返回日志里记录完整堆栈接口只返回“系统繁忙请稍后重试”。用户能看到的具体错误只应该是BizException里写的业务提示。3.4 文件上传与MinIO接入房源图片和合同附件都走文件上传接口。MinIO接入Spring Boot的方式其实不复杂先在pom里引入minio依赖然后在配置类里创建MinioClientConfiguration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }上传文件时先判断桶是否存在不存在就创建然后生成唯一的对象名称。对象名推荐用UUID加原始文件后缀避免文件名冲突也避免中文文件名在URL传参时出现编码问题。返回给前端的URL一般是“MinIO地址/桶名/对象名”前端直接用这个URL渲染img标签即可。关于MinIO的大小我补充一个经验MinIO服务运行内存占用看文件访问量一般给512MB到1GB就够但存储目录一定要挂载在机械硬盘或者SSD的数据盘上不要放系统盘否则系统盘满了整台服务器都会出问题。4. 前端Vue实现与联调前端部分整体用的Vue 2 Element UI没有上TypeScript和Vite原因是这套组合和项目后端的“求稳”思路保持一致生态成熟社区资料多任何组件问题都能快速搜到方案。如果你更习惯Vue 3 Element Plus前端代码迁移成本也不大核心逻辑不变。4.1 Vue工程结构与路由定义前端目录按asrc/views、src/components、src/api、src/router、src/utils来分。views下面按业务模块建子目录比如property、tenant、contract、bill、system每个目录里放对应的列表页和表单页。路由使用懒加载方式按需加载组件避免首屏一次性打包太多文件const routes [ { path: /login, component: () import(/views/login/index.vue), meta: { title: 登录 } }, { path: /, component: Layout, redirect: /dashboard, children: [ { path: property/list, name: PropertyList, component: () import(/views/property/list.vue), meta: { title: 房源管理, icon: home } } ] } ];菜单权限的动态路由在前端路由守卫里实现用户登录成功后请求后端获取该用户的菜单树再把菜单里的组件路径映射成真正的组件对象动态添加到router实例里。这样不同角色登录后看到的左侧菜单天然不同后端也通过拦截器保证接口权限前端隐藏菜单只是体验层面的优化不是安全屏障。4.2 核心页面与组件拆分房源列表页是整个系统的门面数据表格用的是el-table顶部放筛选条件关键词搜索、房源状态下拉、租金区间。表格列展示房源编号、标题、地址、面积、租金、状态标签、封面图缩略图、操作按钮。操作按钮包括编辑、下架、签订合同、退租登记按钮是否显示要由当前状态决定。这里我把“状态标签”抽成了一个公共组件StatusTag。它接收状态值自动映射颜色和文案空置显示绿色“空置”、已预订显示橙色“预订”、已出租显示蓝色“已租”、维修中显示灰色“维修”。这样一个组件在多个页面复用后续如果新增状态只需要改组件里的映射关系不用每个页面都动。表单页面上房源新建和编辑共用同一个组件通过路由参数区分是新增还是编辑。这个做法比复制两个页面要省事得多。对于图片上传前端用的el-upload组件接口指向后端的上传接口上传成功后把返回的URL直接绑定到表单的coverImage字段预览时就用这个URL。4.3 前后端联调与代理配置开发环境最大的坑是跨域。我直接通过vue.config.js里的devServer.proxy解决前端所有/api开头的请求都代理到后端服务地址module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };axios实例的baseURL统一配置为/api请求拦截器里加上token存放位置是localStorage。响应拦截器里统一处理后端返回的code如果是401就跳转到登录页并清除本地用户信息。这样每个页面的接口调用都只需写业务逻辑不需要重复处理鉴权。4.4 打包部署与发布前端打包执行npm run build产物在dist目录。部署方式有两种我强烈建议小项目直接“前端打包放进Spring Boot中运行”。做法是后端resources目录下建一个static文件夹把dist里的静态文件全部拷贝进去再把index.html放到templates目录下。这样整个系统只有一个Java进程不需要额外部署Nginx成本最低。当然要注意接口路径访问时不会去命中静态资源所以后端接口统一加了/api前缀前端静态文件请求的是根路径下的js/css两者不会冲突。如果以后访问量变大想用Nginx部署配置里只需要注意一个点所有非静态文件的请求都要try_files到index.html才能解决history路由刷新404的问题location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }5. 常见问题与排查实录这部分是我在实际整理这个项目时真真切切踩过的坑有些问题看着不起眼但卡住人就是半天。我把它们按频率排个序。5.1 数据库连接与中文乱码中文乱码是最常见的问题。连接MySQL时URL上必须带useUnicodetruecharacterEncodingutf8同时数据库表也要用utf8mb4字符集。有时候改了JDBC连接还是乱码那就是数据库库表本身不是utf8mb4需要执行ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4。还有一个小细节MySQL 8.x的驱动类是com.mysql.cj.jdbc.DriverMySQL 5.x是com.mysql.jdbc.Driver。如果你本地装的是MySQL 8却用了老驱动启动时会报ClassNotFoundException或者连接失败。另外serverTimezone参数也不能丢不设置的话差8个小时日期字段查出来会比实际时间少8小时。5.2 Spring Boot版本过高引发的兼容问题这个项目最初我用过Spring Boot 3.0试跑结果一堆问题。最典型的就是javax.servlet包变成了jakarta.servletMyBatis-Plus老版本启动直接报错。另外springdoc和旧版swagger组件的兼容性也有问题接口文档页面打不开。我的建议是如果你不是必须使用Spring Boot 3的那些新特性业务项目老老实实用2.7.x。如果你确实要用3.x就要同步升级MyBatis-Plus到3.5.5以上、MinIO客户端版本也要换新否则光填依赖坑就会浪费一整天。记住一个原则升级框架版本不是业务需求不值得赶时髦。5.3 Vue跨域与路由刷新404前端本地联调时跨域问题大多靠proxy解决但如果proxy配置写错了vue.config.js改了却不起作用记得要重启devServer。另外注意changeOrigin一定要设置为true不然后端拿到的Host还是前端地址某些后端校验会出现怪异问题。还有一个高发问题前端用的history模式路由直接放到Nginx后刷新子页面会404。原因很简单Nginx找不到对应的静态文件路径。解决方案就是前面提到的try_files配置。如果不想折腾Nginx也可以直接把路由模式改成hash路径会多一个#号但功能一切正常。两种方案我都有过实践经验偏向推荐Nginx配置一次解决。5.4 扩展需求Vue播放m3u8免安装方案我在给这套房屋租赁系统做演示时被问过能不能在房源详情页播放房间实拍视频而且视频源是m3u8格式。m3u8本身就是HLS流媒体的索引文件浏览器原生video标签并不能直接播放所有m3u8流所以需要引入hls.js这个库而且它支持免安装前端打包后直接可用不需要额外装插件。集成方式很简单页面里先安装依赖再在组件中用hls.js创建播放器npm install hls.jsimport Hls from hls.js; function initPlayer(videoEl, src) { if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(src); hls.attachMedia(videoEl); } else if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { videoEl.src src; } }这个方案最省事的地方在于hls.js完全走前端JS实现浏览器端不需要装任何插件iOS上Safari原生支持HLS直接走canPlayType分支即可。做这套扩展时记得把视频文件的访问权限和文件大小纳入考虑MinIO存储视频时单文件限制要在后端multipart配置里调大。6. 文档与二次开发建议源码和数据库都给了如果没有一份像样的文档这个项目对陌生人的可用性会大打折扣。文档的价值不是让作者自我感动而是帮助下一个接手的人省掉摸索成本。6.1 经过我实测的文档必备清单一份能让人照着跑起来的文档至少要有这五块环境要求JDK版本、Maven版本、Node版本、MySQL版本精确到主版本号比如JDK 1.8、MySQL 5.7或8.0。初始化步骤先建库、再导入SQL文件、修改配置文件、启动后端、安装前端依赖、启动前端的完整顺序。我特别强调顺序是因为顺序错了经常出现“看文档没问题跑起来全是坑”。默认账号管理员账号密码、测试租客账号密码方便体验者直接登录。接口说明不需要文档工具生成全套但要列出主要接口的路径、请求方式、关键参数、返回结果。列表、新增、状态流转、上传、登录这五个接口必须写清楚。常见问题FAQ把上面提到的版本不兼容、中文乱码、跨域、刷新404都写进去文档里写着“遇到X问题就检查Y”能少收很多重复提问。6.2 从这套系统扩展成“商用”的改造点这套系统跑通之后如果想要真正商用有几个改造点是我认为优先级比较高的。第一账单模块要增加“逾期提醒”和“催收记录”。现在的账单只是记录应收实收商用还需要在账单到期未付时自动标记逾期并且每次催收电话都要能记录到系统里留痕否则后期对账扯皮没有依据。第二合同模板要支持动态生成和电子签章。目前合同只是存一个合同编号和基本条款说明商用场景需要把合同条款模板化通过数据填充生成PDF再对接简单的电子签名能力哪怕先用图片签也可以接受。第三权限粒度要细化到“数据范围”。现在管理员能看到所有房源如果商用后有几个股东或几个门店需要支持“这个账号只能查看自己门店的房源、合同和账单”。实现方式就是用户表加dept_id所有核心表也加dept_id查询时自动追加过滤条件。第四前端最好增加租客微信端的查看功能。租客通过小程序查看自己房子的账单、报修进度、合同到期时间能显著减少运营人员的沟通成本。后端接口是现成的小程序端只是重新包一层UI算是性价比很高的扩展方向。最后说点个人体会。这种小型管理系统技术难点真的不是某个框架的某个API而是能不能把业务状态想清楚、把表结构设计好。我做这个项目时第一版就是先把房源和租客两张表建完直接写页面结果做到合同模块发现没有地方记录押金和欠费被迫回炉再造。后来老老实实把业务实体、状态流转、权限模型画清楚再动手后面所有模块都顺了很多。这就是为什么我一直建议项目越“小”设计越不能省。
返回列表