☰
SpringBoot+Vue房屋租赁管理系统开发实战:从设计到部署
2026/10/6 3:14:45 网站建设 项目流程

做开发这些年,我越来越觉得“业务管理系统”这类项目是最锻炼人的——功能看着不大,但角色、状态、流程一多,设计上的毛病就会立刻暴露出来。最近我刚完整跑通一套基于SpringBoot+Vue的房屋租赁管理系统,后端Java,数据库MySQL,持久层用MyBatis,前端Vue做页面和交互,从数据库建表到前后端联调再到打包部署,全是自己一点点调的。这套系统解决的是租赁业务里的那些“钻头”问题:房东怎么发布房源、租客怎么在线找房、签约下单、租金结算、退租后房子怎么重新上架,全部在一条链路上跑完。这篇文章我会把这套系统的整个设计思路、表结构、关键代码和调试过程都拆开讲透。如果你正准备用SpringBoot+Vue做毕业设计,或者刚接到一个外包单子需要快速搭出一套可用的管理平台,这篇文章可以直接拿来当参考底稿。

房屋租赁和普通的商品交易还不一样,房子是长期资产,涉及的角色多,状态也更复杂。同一个房源,从房东录入、管理员审核、租客看房、下单、签约、入住,到最后退租释放,中间任何一步断了,整条业务就卡住。所以这类系统的核心不是“把增删改查写完”,而是把角色的权限边界、房屋的状态流转、订单的支付流程都理清楚,才有资格谈实现。

1. 项目整体设计与思路拆解

1.1 技术栈选型的底层逻辑

先回答一个很多人会问的问题:为什么是SpringBoot+Vue,而不是SSH、不是JSP、也不是前后端不分离的老式写法?

SpringBoot在Java后端生态里已经是事实上的标准,它把Spring繁琐的XML配置全部收进自动配置机制里,内嵌Tomcat,一个mvn package打出来的jar包直接java -jar就能跑,对中小型业务系统来说部署成本极低。MyBatis作为持久层框架,和Hibernate/JPA最大的区别在于SQL是掌握在开发者自己手里的。房屋租赁系统里有很多多表联查、动态条件筛选的场景,比如“按价格区间+户型+地址模糊搜索房源”,用MyBatis的动态SQL写起来非常直观,排查问题时也能直接拿SQL去数据库验证,效率高很多。MySQL则不多解释,这类业务的数据量在一段时间内根本到不了需要上分布式数据库的程度,单库单表加几个索引完全够用。

前端选择Vue的原因也很实际。Vue的生态对国内开发者非常友好,Element UI、Vant、Ant Design Vue这些组件库都是开箱即用,后台管理系统的列表、表单、弹窗、分页这些高频组件拖进来就能跑。Vue Router和Vuex在路由控制和状态管理上也有成熟方案,而且Vue的响应式机制和模板语法上手难度比React低,团队协作时沟通成本也小。

这整套技术组合还有一个隐性优势:资料多、踩坑记录多。说实话,做业务系统最怕的不是功能复杂,而是遇到一个冷门问题查半天查不到解决方案。这套组合从安装配置到代码实现,网上几乎能找到所有问题的答案,开发效率和后期维护都会省心不少。

1.2 系统角色与核心业务流程拆解

我设计这个系统时,一开始就确定了三类角色:管理员、房东、租客。权限是分开的,页面和接口也是分开的,这是管理系统的基本盘。

管理员负责平台侧的维护工作:审核房东提交的房源、管理注册用户、发布平台公告、查看所有订单数据。房东和租客注册后默认状态正常,管理员从后台能看到所有用户列表,必要时可以禁用某个账号。

房东的核心操作是房源管理:发布房源、编辑房源信息、查看自己房源收到的订单、确认租客下单后进入签约流程、租期结束后办理退租。这里有一个细节值得注意:房东发布的房源不是直接上架的,而是先进入“待审核”状态,管理员审核通过后租客才看得到,避免有人乱发重复、虚假房源。

租客的核心操作是找房和租房:登录后浏览房源、按条件搜索、收藏房源、发起租房请求、支付押金和租金、在线签署合同、租期结束后发起退租申请。

整个系统的核心链路可以这样走:房东发布房源,管理员审核通过,房源状态变为上架;租客浏览房源并下单,此时订单处于待支付状态;租客模拟支付后,订单变为已支付,房东可确认;房东确认后生成电子合同,订单和房源进入租赁中状态;租期结束申请退租,房东确认后退还押金,订单关闭,房源重新变为上架状态。

这里最关键的是一张状态流转表,后端的接口逻辑里每一处状态变更都必须符合这张表的规则,否则就会出现“房子已经租出去了,列表还显示可租”的脏数据。

房源状态码含义触发动作
0待审核房东发布房源
1上架中管理员审核通过
2已出租租客支付成功、房东确认
3已下架房东主动下架或管理员下架
订单状态码含义触发动作
0待支付租客提交订单
1已支付租客完成支付
2租赁中房东确认订单、合同签署
3已退租租客退租、房东确认
4已取消租客取消或超时未支付

这张表看起来简单,但它是整个系统设计里最值得花时间的部分。状态流转理不清楚,后面写接口的时候就是一团乱麻。

2. 数据库设计与核心表结构

2.1 核心表设计与字段说明

数据库设计我建议直接决定业务的上限。表结构建得不合理,后面写SQL、联表、做统计都会不舒服。这套系统的核心表我划分成六张:用户表、房屋表、租赁订单表、合同表、收藏表、支付记录表。下面挑最重要的几张表展开说。

用户表是系统的基础,建议直接写成:

CREATE TABLE `t_user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(255) NOT NULL, `real_name` varchar(50) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `role` tinyint NOT NULL DEFAULT '2' COMMENT '0管理员 1房东 2租客', `avatar` varchar(255) DEFAULT NULL, `is_deleted` tinyint NOT NULL DEFAULT '0', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

房屋表是核心业务表,字段要覆盖基本信息、价格、地址和状态:

CREATE TABLE `t_house` ( `id` int NOT NULL AUTO_INCREMENT, `landlord_id` int NOT NULL, `title` varchar(100) NOT NULL, `description` text, `area` decimal(8,2) NOT NULL COMMENT '面积(平米)', `rent` decimal(10,2) NOT NULL COMMENT '月租金', `deposit` decimal(10,2) DEFAULT NULL COMMENT '押金', `house_type` tinyint DEFAULT NULL COMMENT '户型:0一室 1两室 2三室 3其它', `address` varchar(255) NOT NULL COMMENT '详细地址', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待审核 1上架中 2已出租 3已下架', `is_deleted` tinyint NOT NULL DEFAULT '0', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_landlord_id` (`landlord_id`), KEY `idx_status` (`status`), KEY `idx_rent` (`rent`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表是业务流程的状态载体,直接决定业务流转能否记录清楚:

CREATE TABLE `t_lease_order` ( `id` int NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `house_id` int NOT NULL, `tenant_id` int NOT NULL, `landlord_id` int NOT NULL, `start_time` date DEFAULT NULL, `end_time` date DEFAULT NULL, `rent` decimal(10,2) NOT NULL, `deposit` decimal(10,2) NOT NULL, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2租赁中 3已退租 4已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_house_id` (`house_id`), KEY `idx_tenant_id` (`tenant_id`), KEY `idx_landlord_id` (`landlord_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

合同表主要保存合同文本和签订状态,与订单表一对一关联。收藏表则记录租客收藏的房源,字段包括用户ID和房源ID,用于“我的收藏”页面展示。支付记录表记录每一次支付流水,包括订单号、支付金额、支付方式、支付时间,虽然这个项目里支付是模拟的,但表结构仍然按真实场景来设计,后面接微信或支付宝支付时只需要替换支付实现,不需要改表。

2.2 字段、索引与约束设计的关键细节

建表除了把字段列出来,还有几个细节直接影响使用体验和性能。

第一个细节:金额字段用DECIMAL(10,2),绝不能用float或double。浮点类型在MySQL里会有精度丢失问题,租金和押金这种对精确度敏感的数据,哪怕差一分钱在账目上都是麻烦事。

第二个细节:状态字段用tinyint配合数字常量,不要用字符串。字符串看似直观,但存储空间更大,而且很容易因为大小写问题导致判断失误。项目里我会定义一个状态常量类或枚举类,把所有状态码统一管理,代码里不让魔法数字乱飞。

第三个细节:逻辑删除字段is_deleted值得加。租赁系统有比较强的业务关联性,比如房东手里可能有多套房源、订单关联着合同和支付记录,物理删除一条数据很容易导致关联数据悬空。我的做法是所有核心表都加is_deleted字段,删除操作统一改成UPDATE ... SET is_deleted = 1,查询时默认过滤已删除数据。这样即使误操作,数据也能救回来。

第四个细节:外键约束我不建议在数据库层面加。MyBatis项目里一般通过Java代码维护表之间的关系,订单表里的house_id、tenant_id等字段在业务层做逻辑校验,而不在数据库层建立物理外键。原因很现实——物理外键的维护成本在高并发写入场景下很高,而且在进行逻辑删除、批量操作时容易被数据库的约束卡住。只要保证索引存在,查询性能就不会有问题。

第五个细节:索引不要滥用。房屋表我建了landlord_id、status、rent三个单列索引,因为实际查询最高频的就是按这三个条件筛选房源。订单表则对house_id、tenant_id、landlord_id建了索引,方便三边查询各自的订单列表。值得注意的是,address字段虽然是搜索条件,但因为常用的是模糊查询LIKE '%地址%',这种查询是没办法走索引的,所以我没有对它单独建索引,倒是在查询时配合is_deleted做过滤就够了。

2.3 初始化数据与账号规划

一套系统不能没有测试账号,尤其是管理后台。我会在SQL初始化脚本里预置一个管理员账号、一个房东账号、一个租客账号,密码统一用BCrypt加密后的密文写入。同时插入几条房屋测试数据,方便前端开发时直接调接口看效果。

初始化数据还有一个小技巧:把默认管理员账号的username设为admin,房东设为landlord01,租客设为tenant01,然后把这些账号信息写在项目README里。这样无论是自己测试还是交给别人验收,都能快速进入业务演示阶段,不用从头注册。

3. 后端核心实现:SpringBoot与MyBatis落地细节

3.1 项目骨架搭建与配置

我创建后端项目时用的是Maven,JDK版本建议看你的SpringBoot版本:如果是SpringBoot 2.x,JDK 8完全够用;如果用了SpringBoot 3.x,就必须上JDK 17,而且要注意javax.servlet包已经改成了jakarta.servlet,自己写Filter或拦截器时很容易在这里踩坑。这里我建议一般业务系统直接选SpringBoot 2.7.x加JDK 8,兼容性最好,资料也最多。

pom.xml里的核心依赖大概是这样的:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

application.yml里的关键配置也需要留个心,尤其是数据库连接参数和MyBatis扫包路径:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/rental_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8 username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.rental.entity configuration: map-underscore-to-camel-case: true

这里特别提醒一下数据库连接URL里的几个参数。useSSL=false是为了避免本地开发时MySQL SSL握手导致的连接告警;serverTimezone=Asia/Shanghai解决时区问题,否则日期时间字段会差8个小时;allowPublicKeyRetrieval=true是MySQL 8.0及以上版本在非SSL连接下需要额外允许获取公钥,否则可能报Public Key Retrieval is not allowed的错误。这些参数不配全,系统启动后连数据库那一步就会把人卡死。

3.2 统一响应结构与全局异常处理

后端接口返回结构如果不统一,前端处理数据时就要写一堆判断。我的做法是封装一个Result对象,所有接口都返回这个结构:

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.code = code; result.message = message; return result; } }

同时用@RestControllerAdvice做全局异常捕获。业务里抛出的自定义异常、参数校验异常、数据库异常,统一转成Result格式返回给前端。这样前端只需要在axios响应拦截器里判断code字段,小于0的统一弹错误提示,不用每个接口各写一套try-catch。

3.3 MyBatis动态SQL与分页查询

房屋列表页是核心查询场景,租客可能按关键字搜索、按价格区间筛选、按户型筛选、按状态筛选,这些条件组合起来非常多。如果在Java代码里拼接SQL字符串,代码会非常丑陋且容易出SQL注入问题。MyBatis的动态SQL标签是最合适的方案。

我在Mapper XML里这样写房屋列表的查询:

<select id="selectHouseList" resultType="com.rental.entity.House"> SELECT * FROM t_house <where> <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="minRent != null"> AND rent &gt;= #{minRent} </if> <if test="maxRent != null"> AND rent &lt;= #{maxRent} </if> <if test="houseType != null"> AND house_type = #{houseType} </if> <if test="status != null"> AND status = #{status} </if> AND is_deleted = 0 </where> ORDER BY create_time DESC </select>

这里有两个细节要强调。第一,<where>标签会自动去掉第一个条件前的AND,所以每个<if>里的SQL都以AND开头是没有问题的,但如果某个条件在SQL里是<、>这类符号,必须用&lt;和&gt;进行转义,否则XML解析会直接报错。第二,LIKE模糊查询我这里用CONCAT('%', #{keyword}, '%'),不要直接在参数里拼%再传入,这样可以更清晰看到参数实际值,调试也方便。

分页我用PageHelper插件,用法很固定:

PageHelper.startPage(pageNum, pageSize); List<HouseVO> list = houseMapper.selectHouseList(query); PageInfo<HouseVO> pageInfo = new PageInfo<>(list);

PageInfo里包含了总条数、当前页码、每页条数等完整分页信息,前端只需要把它返回,Vue的表格组件就能直接渲染。使用PageHelper有一个必须注意的规则:PageHelper.startPage()必须紧接着执行下一条SQL查询语句,中间不能夹其他SQL操作,否则分页效果会串到别的查询上,导致数据莫名其妙变少或分页失效。

3.4 房屋上下架、下单与状态流转实现

状态流转是这个系统的业务核心,我重点说下单这个动作,因为它涉及两张表的状态修改,最容易出并发问题。

租客点击“立即租房”后,后端Service做了这些事:

@Override @Transactional(rollbackFor = Exception.class) public OrderCreateResult createOrder(OrderCreateDTO dto) { // 1. 查询房屋信息,这里使用悲观锁保证并发安全 House house = houseMapper.selectByIdForUpdate(dto.getHouseId()); if (house == null || house.getStatus() != HouseStatus.ON_SALE) { throw new BusinessException("房源不存在或已被预定"); } // 2. 生成唯一订单号 String orderNo = generateOrderNo(); // 3. 插入订单表 LeaseOrder order = new LeaseOrder(); order.setOrderNo(orderNo); order.setHouseId(house.getId()); order.setTenantId(dto.getTenantId()); order.setLandlordId(house.getLandlordId()); order.setRent(house.getRent()); order.setDeposit(house.getDeposit()); order.setStatus(OrderStatus.PENDING_PAYMENT); leaseOrderMapper.insert(order); // 4. 修改房源状态为已出租(条件更新,防止并发) int rows = houseMapper.updateStatusConditionally( house.getId(), HouseStatus.OFF_SALE, HouseStatus.ON_SALE); if (rows == 0) { throw new BusinessException("房源状态已变化,请刷新后重试"); } return new OrderCreateResult(orderNo); }

这里有两个细节值得展开。第一,selectByIdForUpdate是加行级锁的查询,它的作用是:同一时间只有一个请求能查到这条房源记录并进入后续流程,其他并发请求会阻塞等待。配合@Transactional事务,整个“查房屋+插订单+改状态”的过程是一个原子操作,不会出现两个租客同时下单成功的情况。第二,updateStatusConditionally是条件更新:

<update id="updateStatusConditionally"> UPDATE t_house SET status = #{newStatus} WHERE id = #{houseId} AND status = #{oldStatus} </update>

这种“先查询后条件更新”的组合既保证了数据一致,又不至于完全靠数据库锁把并发堵死,是实际项目中很常见的写法。

订单支付成功后,订单状态更新为已支付,此时房东可以确认订单。房东确认后,系统自动生成合同记录,订单状态变成租赁中,房屋状态保持已出租。退租时,租客发起退租申请,房东确认后,订单状态变成已退租,房屋状态重新变为上架中,同时生成一笔押金退还记录。这些状态变更逻辑都是类似的条件更新,每走一步都要判断当前状态是否符合预期,防止非法跳转。

3.5 事务、参数校验与权限控制

后端开发时事务是很容被忽视的一环。凡是涉及多表写入的操作,@Transactional(rollbackFor = Exception.class)必须加上。比如创建订单时同时插入订单表和更新房屋表,如果房屋状态更新失败但订单已经插入,事务不回滚的话,数据库里就会多出一条脏订单。这里rollbackFor指定了Exception.class,表示无论运行时异常还是受检异常都触发回滚,避免某些异常类型不会自动回滚的坑。

参数校验可以用@Validated加@NotNull、@NotBlank这类注解,在Controller入口直接拦截非法参数。还有权限控制,虽然前端做了路由守卫,但后端必须自己再校验一次。我的做法是写一个拦截器,根据登录用户的身份判断某个接口是否可访问,比如租客只能操作自己名下的订单,房东只能管理自己发布的房源,不能通过篡改请求参数来操作别人的数据。

4. 前端核心实现:Vue与Element UI联动

4.1 Vue项目初始化与目录规划

前端用Vue CLI创建项目,命令是vue create rental-web,考虑到主流浏览器兼容性,我选的Vue 2搭配Element UI。Element UI在Vue 2生态下非常成熟,表格、表单、弹窗、分页、消息提示这些组件正好覆盖管理系统的绝大多数场景。如果你用Vue 3,对应的是Element Plus,用法类似,但API细节有些差异,选型时确认好版本就行。

安装完成后,我会把目录结构划分清楚,业务模块按角色拆:

src ├── api │ ├── auth.js │ ├── house.js │ ├── order.js │ └── user.js ├── router │ └── index.js ├── store │ └── modules ├── views │ ├── admin │ │ ├── HouseAudit.vue │ │ └── UserManage.vue │ ├── landlord │ │ ├── PublishHouse.vue │ │ └── HouseList.vue │ ├── tenant │ │ ├── HouseSearch.vue │ │ └── MyOrder.vue │ └── Login.vue └── utils └── request.js

把api单独抽成一个目录,每个模块的接口地址集中管理,改动后端接口时只需要改一个文件,不用到处找。这个习惯越早养成,写大项目的时候越省力。

4.2 路由配置与登录守卫

路由分两类:公共路由和需要登录才能访问的路由。登录、注册、首页房源浏览这几种页面是公共的;后台管理员页面、房东页面、租客个人页面都要求登录。

我在router/index.js里配置了路由meta信息,通过requiresAuth标记是否需要登录,通过role标记允许访问的角色。

const routes = [ { path: '/login', component: Login }, { path: '/', component: Home }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, role: 'admin' }, children: [ { path: 'house-audit', component: HouseAudit } ] }, { path: '/landlord', component: LandlordLayout, meta: { requiresAuth: true, role: 'landlord' }, children: [ { path: 'publish', component: PublishHouse } ] } ]

路由守卫用的是beforeEach,每次跳转前检查本地存储里的登录状态和角色信息,不符合条件的跳回登录页。这里必须强调一句:前端守卫只解决体验问题,真正的权限控制绝对要在后端做。前端路由守卫生效,只是看不到页面而已,绕过前端直接调接口是很容易的事,所以后端接口层面的鉴权不能省。

4.3 axios请求封装与状态管理

前端和后端交互统一走axios,我会封装一个request.js,把基础配置、请求拦截、响应拦截统一处理:

import axios from 'axios' import { Message } from 'element-ui' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:附加登录token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) // 响应拦截器:统一处理业务状态码 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { Message.error('网络请求异常') return Promise.reject(error) } ) export default request

登录后的用户信息统一放在Vuex里,包括用户名、角色、ID,刷新页面后再从接口获取一次完整信息。Vuex的使用不要太复杂,管理系统里一般就是存用户信息和登录状态,保持data字段小而清晰就够了。

4.4 核心页面设计与接口联调

房源列表页是租客看到的第一个核心页面。我用Element UI的el-card展示房源卡片,上面是房屋图片,下面是标题、面积、租金、地址,点击进入详情页。筛选栏用的是el-form内联布局,包括关键字输入框、价格区间输入、户型下拉框、搜索按钮,搜索时统一把条件参数通过axios传给后端,后端返回分页数据。

分页这块必须讲一下PageHelper返回格式和前端表格的对接。后端PageInfo返回的数据是:

{ "total": 35, "list": [...], "pageNum": 1, "pageSize": 10 }

前端在表格或卡片列表里渲染时,直接绑定list,分页组件里把total赋给total属性,当前页和每页条数分别赋给current-page和page-size,切换页码时重新带参查询即可。一些新手容易踩的坑是:后端返回的list字段被axios拦截器剥掉了一层data,结果前端拿到的直接是list还是{total, list},这个要看自己的封装逻辑,接口联调时先用浏览器的Network面板确认返回结构再写代码,不要凭感觉。

房东端的“发布房源”页面主要是表单提交,包括标题、描述、面积、租金、押金、户型、地址、上传图片。图片上传我做成单独接口,前端用el-upload把文件传到后端,后端保存文件后返回可访问的URL,再把URL拼在房源信息里一起提交。表单提交前用el-form的rules做好必填校验,减少后端压力。

管理员端的房源审核页则是一个典型的管理列表:表格展示所有待审核房源,操作列有“通过”和“驳回”按钮,点击后调后端审核接口,前端再刷新当前列表。

4.5 前端打包与部署方式

本地开发时,Vue项目直接起在8081端口,后端在8080端口,跨域是绕不开的问题。我的做法是在vue.config.js里配置devServer代理:

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

这样前端请求/api/house/list时,开发服务器会把请求转发到后端的/house/list,浏览器的Network里看到的是同源请求,不会触发跨域拦截。

生产部署有两种常用方式。第一种是直接把前端构建产物放到SpringBoot项目的src/main/resources/static目录下,重新打包后jar包自带页面,访问http://ip:8080就能打开系统,适合单机小规模部署。第二种是用Nginx托管前端静态文件,同时把/api反向代理到后端的8080端口,适合前后端需要独立扩缩容的场景。推荐新手先用第一种方式,部署链路最短,也最容易排查问题。

5. 常见问题与排查技巧实录

5.1 数据库连接与启动报错

这个项目最容易在启动阶段卡住的就是数据库连接。常见报错有这几种:

一是Failed to configure a DataSource,通常是URL、用户名、密码没配置对,先检查application.yml里的数据源配置和实际数据库是否匹配。

二是SSL connection error,MySQL 8默认开启SSL,本地测试时为了省事直接在URL后面加useSSL=false。

三是Public Key Retrieval is not allowed,这个问题在MySQL 8.0以上版本经常出现,原因是客户端使用caching_sha2_password认证时需要从服务端获取公钥,URL里加allowPublicKeyRetrieval=true就能解决。

四是时区错乱,serverTimezone=Asia/Shanghai必须加,否则插入的时间字段会比当前时间少8小时,排查起来非常隐蔽。

补充一个MySQL安装相关的小坑。Windows上安装MySQL时,如果之前装过旧版本,很容易出现服务无法启动或端口被占用的现象。安装5.7版本时记得先检查C:\ProgramData\MySQL目录下是否有残留配置,安装8.0版本时如果初始化失败,可以把安装目录下的data文件夹备份后删除,再重新执行初始化命令,很多诡异的启动问题都能解决。

5.2 MyBatis映射文件绑定失败

遇到Invalid bound statement (not found)的报错,十有八九是Mapper接口和XML映射文件没有对得上。排查顺序是:第一,检查application.yml里的mybatis.mapper-locations是否指到了正确的目录,比如classpath:mapper/*.xml;第二,检查XML文件的namespace是否和Mapper接口的全限定名一致;第三,检查Mapper接口里的方法名是否和XML里<select>/<update>标签的id一致;第四,检查打包后target/classes目录下是否真的生成了XML文件,Maven默认只把src/main/resources下的文件打进classpath,如果XML放在src/main/java下就需要额外配置。

还有一个特别容易忽略的问题:项目里如果同时引入了mybatis-spring-boot-starter和mybatis-plus-boot-starter,两种框架的Mapper扫描机制会互相干扰,导致XML文件解析不到或Mapper Bean重复注册。小项目里不要在依赖里把MyBatis和MyBatis-Plus混用,选一个就好。

5.3 PageHelper分页失效

PageHelper分页插件失效通常是两种原因。第一种是PageHelper.startPage()后面没有紧跟第一条要分页的SQL查询,中间插了其他数据库操作,分页条件就被其他查询吃掉了。第二种是项目里使用了我前面提到的“先查后改”这种逻辑,比如先查询房屋再插入订单,如果在分页操作之后又执行了其他SQL,分页就会失效,所以写出满足分页的SQL时要注意调用顺序。

还有一种情况是PageHelper和自定义的Mapper方法使用了resultMap联表查询时,分页能生效,但统计的总条数可能不对。这是因为PageHelper会自动生成count语句,如果联表查询里有GROUP BY或者DISTINCT,自动生成的count可能没有包含这些关键词,导致总条数和实际不同。这时候可以用PageHelper.startPage(pageNum, pageSize, true)强制走智能count,或者自己写一个专门的count查询。

5.4 前后端跨域问题

本地开发如果没走代理,直接让前端请求后端的接口,控制台会报跨域错误。解决办法有两种,二选一即可。后端方案是配置一个CorsFilter全局跨域过滤器:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

前端方案就是我前面说的devServer代理。实际项目里我更推荐前端代理方案,因为后端的跨域配置会把接口暴露给所有来源,如果后续要做严格的域名限制,改动会更麻烦。

5.5 日期时间显示与JSON序列化

接口返回的数据里,LocalDateTime类型字段默认序列化出来是一长串带T的时间格式,前端直接显示会非常难看,而且有时还会出现时区偏移。我习惯在application.yml里统一配置JSON序列化格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

这样后端返回的时间字符串就是2025-01-15 14:30:00这种格式,前端拿来就能直接展示,不用再调一遍格式。如果是日期字段,比如合同起止时间,前端组件用el-date-picker时要注意绑定值类型,Value-format设置为yyyy-MM-dd才能保证提交给后端时不带时间部分。

5.6 SpringBoot版本过高引发的兼容性问题

现在在创建新项目时,IDE默认拉取的SpringBoot版本往往是3.x,但SpringBoot 3.x有两个变化特别容易影响老项目迁移:一是强制要求JDK 17,二是Servlet包名从javax.servlet变成了jakarta.servlet。如果你用的是JDK 8,直接拉到3.x版本的项目根本起不来,启动时会报UnsupportedClassVersionError。我的建议是:环境是JDK 8就显式把SpringBoot版本改成2.7.x,环境是JDK 17再用3.x,不要盲目追新版本。

另外热词里提到“vue打包放进springboot中”,这一步其实很简单,但要注意SpringBoot对静态资源的处理逻辑。把Vue构建出的dist目录里的所有文件复制到src/main/resources/static下后,不要保留dist这层文件夹,直接让index.html位于static根目录下。否则访问http://ip:8080/时定位不到页面,还要多一层路径。

6. 从零到一:完整开发节奏建议

最后说一点我个人的实操体会。这种系统一开始最容易犯的错误就是“先写代码再想逻辑”,结果做到一半发现状态流转对不上,返工成本极其高昂。我自己的节奏是先花一个晚上把业务角色和状态流转表画清楚,然后直接建库建表,把六张核心表全部初始化好,再开始写后端接口。后端接口按业务链路顺序写:用户登录、房源发布、列表查询、下单、支付、确认订单、生成合同、退租,每完成一个接口就先用Postman自测一遍,确认返回结果正确后,再动前端页面。

前端部分也是同样的节奏:先做登录页和管理后台骨架,把接口调通一次,确认请求能通、数据能渲染,再逐渐丰富页面细节。这样做的好处是,整个开发过程中任何一个环节出问题,都能迅速定位到是后端问题、SQL问题还是前端联调问题,而不是等到项目快收尾时才集中爆发一堆连接错误和状态不一致。

还有一个非常值得养成的习惯:所有状态枚举值和常量统一放在一个constant包里,前端页面里展示的状态标签也通过一个dict.js文件统一映射,保持前后端的“字典表”始终一致。比如后端返回house.status=1,前端才可以根据statusMap[1]渲染成“上架中”标签。如果前后端各写各的,状态值一改,页面显示就会错乱。

这套系统做完以后,你如果还想继续扩展,优先考虑的方向有两个。第一是引入Redis做房屋浏览量和热门房源的缓存,缓解数据库压力;第二是接入真实支付渠道,把模拟支付替换成微信或支付宝的统一下单和异步回调逻辑。这两个方向都是在现有架构上做增量改造,不会推翻已有的设计,而且收益非常明显。

做项目最忌讳的就是照着视频一行行敲代码,敲完还是什么都不会。这套房屋租赁管理系统的价值,在于它有清晰的角色边界、完整的业务状态流转、真实的联调场景,你只要把它从头到尾自己设计和实现一遍,SpringBoot、MyBatis、Vue这套实用技术栈的实战能力,基本就能上一个明显的台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询