SpringBoot+Vue房屋租赁管理系统开发实战:从设计到部署
发布时间:2026/10/6 3:14:48 作者:尧图编辑部 阅读量:1,286

做开发这些年我越来越觉得“业务管理系统”这类项目是最锻炼人的——功能看着不大但角色、状态、流程一多设计上的毛病就会立刻暴露出来。最近我刚完整跑通一套基于SpringBootVue的房屋租赁管理系统后端Java数据库MySQL持久层用MyBatis前端Vue做页面和交互从数据库建表到前后端联调再到打包部署全是自己一点点调的。这套系统解决的是租赁业务里的那些“钻头”问题房东怎么发布房源、租客怎么在线找房、签约下单、租金结算、退租后房子怎么重新上架全部在一条链路上跑完。这篇文章我会把这套系统的整个设计思路、表结构、关键代码和调试过程都拆开讲透。如果你正准备用SpringBootVue做毕业设计或者刚接到一个外包单子需要快速搭出一套可用的管理平台这篇文章可以直接拿来当参考底稿。房屋租赁和普通的商品交易还不一样房子是长期资产涉及的角色多状态也更复杂。同一个房源从房东录入、管理员审核、租客看房、下单、签约、入住到最后退租释放中间任何一步断了整条业务就卡住。所以这类系统的核心不是“把增删改查写完”而是把角色的权限边界、房屋的状态流转、订单的支付流程都理清楚才有资格谈实现。1. 项目整体设计与思路拆解1.1 技术栈选型的底层逻辑先回答一个很多人会问的问题为什么是SpringBootVue而不是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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;房屋表是核心业务表字段要覆盖基本信息、价格、地址和状态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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表是业务流程的状态载体直接决定业务流转能否记录清楚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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;合同表主要保存合同文本和签订状态与订单表一对一关联。收藏表则记录租客收藏的房源字段包括用户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 项目骨架搭建与配置我创建后端项目时用的是MavenJDK版本建议看你的SpringBoot版本如果是SpringBoot 2.xJDK 8完全够用如果用了SpringBoot 3.x就必须上JDK 17而且要注意javax.servlet包已经改成了jakarta.servlet自己写Filter或拦截器时很容易在这里踩坑。这里我建议一般业务系统直接选SpringBoot 2.7.x加JDK 8兼容性最好资料也最多。pom.xml里的核心依赖大概是这样的dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml里的关键配置也需要留个心尤其是数据库连接参数和MyBatis扫包路径server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/rental_system?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8 username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.rental.entity configuration: map-underscore-to-camel-case: true这里特别提醒一下数据库连接URL里的几个参数。useSSLfalse是为了避免本地开发时MySQL SSL握手导致的连接告警serverTimezoneAsia/Shanghai解决时区问题否则日期时间字段会差8个小时allowPublicKeyRetrievaltrue是MySQL 8.0及以上版本在非SSL连接下需要额外允许获取公钥否则可能报Public Key Retrieval is not allowed的错误。这些参数不配全系统启动后连数据库那一步就会把人卡死。3.2 统一响应结构与全局异常处理后端接口返回结构如果不统一前端处理数据时就要写一堆判断。我的做法是封装一个Result对象所有接口都返回这个结构Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT 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 idselectHouseList resultTypecom.rental.entity.House SELECT * FROM t_house where if testkeyword ! null and keyword ! AND title LIKE CONCAT(%, #{keyword}, %) /if if testminRent ! null AND rent gt; #{minRent} /if if testmaxRent ! null AND rent lt; #{maxRent} /if if testhouseType ! null AND house_type #{houseType} /if if teststatus ! 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); ListHouseVO list houseMapper.selectHouseList(query); PageInfoHouseVO 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 idupdateStatusConditionally 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 errorMySQL 8默认开启SSL本地测试时为了省事直接在URL后面加useSSLfalse。三是Public Key Retrieval is not allowed这个问题在MySQL 8.0以上版本经常出现原因是客户端使用caching_sha2_password认证时需要从服务端获取公钥URL里加allowPublicKeyRetrievaltrue就能解决。四是时区错乱serverTimezoneAsia/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: GMT8这样后端返回的时间字符串就是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.status1前端才可以根据statusMap[1]渲染成“上架中”标签。如果前后端各写各的状态值一改页面显示就会错乱。这套系统做完以后你如果还想继续扩展优先考虑的方向有两个。第一是引入Redis做房屋浏览量和热门房源的缓存缓解数据库压力第二是接入真实支付渠道把模拟支付替换成微信或支付宝的统一下单和异步回调逻辑。这两个方向都是在现有架构上做增量改造不会推翻已有的设计而且收益非常明显。做项目最忌讳的就是照着视频一行行敲代码敲完还是什么都不会。这套房屋租赁管理系统的价值在于它有清晰的角色边界、完整的业务状态流转、真实的联调场景你只要把它从头到尾自己设计和实现一遍SpringBoot、MyBatis、Vue这套实用技术栈的实战能力基本就能上一个明显的台阶。