ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue+MyBatis的房屋租赁管理系统实现全解析

基于SpringBoot+Vue+MyBatis的房屋租赁管理系统实现全解析 做过不少Java Web项目之后你会发现SpringBoot Vue MyBatis MySQL这套组合在中小型管理系统里依然是最稳、最省心的搭配。最近刚好把一套房屋租赁管理系统从零到一完整跑通从架构设计、表结构、接口划分到前端联调、部署上线整个流程里踩了不少坑也沉淀了不少经验。这篇就把整个项目拆开讲给准备做同类系统毕业设计、课设、或者小公司内部系统的朋友一个可以直接抄作业的参考。这个系统覆盖了租赁业务的完整闭环房东发房源、管理员审核、租客浏览签约、账单缴费、报修处理、退租结算全都有。技术层面不算难但胜在五脏俱全——前端Vue Element UI做界面后端SpringBoot提供接口MyBatis操作MySQL三层架构清晰。如果你手上正缺一个能快速跑起来、逻辑完整的管理系统项目做练手或交付这套东西的骨架子非常值得参考。1. 项目整体设计先搞清楚它到底要解决什么问题1.1 核心角色和业务闭环做任何项目第一步永远是理清角色和业务流程而不是急着建表、写代码。房屋租赁管理系统的核心角色有3个管理员、房东、租客。我第一次做这类系统时就吃过亏——上来就埋头设计表结构结果做到一半发现业务流程根本推不动返工了两次。所以下面这个闭环必须先想清楚房东登录后发布房源上传图片、地址、价格、面积、户型等管理员审核房源审核通过后房源才会公开上架租客浏览房源、收藏、提交租赁申请房东确认申请后生成租赁合同记录起止时间、租金、押金系统根据合同周期自动生成账单房租、水电费等租客在线确认缴费租客入住后可以发起报修房东处理并反馈合同到期可以续约或退租退租时结算押金和欠费。这一套流程里权限边界很清晰租客只能操作自己的申请和账单房东只能管理自己的房源和租客合同管理员拥有最高权限。如果你只是做一个演示级别的项目可以简化掉部分功能但角色权限这层建议保留因为在简历或答辩里这是一个很容易被追问的点。1.2 技术选型逻辑为什么还是SpringBoot Vue MyBatis我知道有人会说现在微服务、前后端分离都这么流行了为什么还要用这套老组合但实际下来这套组合在中小型项目里确实是最优解我说几个你写代码时能实际感受到的理由SpringBoot把配置简化到极致不需要复杂XML一个Application类就能启动整个服务开发效率很高。Vue的双向数据绑定和组件化开发写后台管理界面比JSP时代爽太多了Element UI拖过来就能用页面效果很拿得出手。MyBatis半自动ORM在手写复杂查询时非常灵活比如房源列表的多条件筛选动态SQL能直接在XML里搞定。MySQL部署简单大部分人本地环境现成就有不用为数据库引入额外学习成本。如果你追求开箱即用的效果这套组合能让你至少节省一半的开发时间。反过来说这个项目里我之所以没有用Spring Data JPA是因为租赁系统的查询条件组合比较多——价格区间、区域筛选、面积范围、发布时间排序这些用MyBatis动态SQL比JPA派生的查询方法直观得多。2. 数据库设计六张核心表撑起整个业务2.1 表结构总览数据表设计是这个系统的地基表建好了后面所有功能都是在上面添砖加瓦。我最终拆了6张核心表把之前很多人在设计中容易犯的一张表塞所有字段的毛病直接避开。下面这张表是整体概览建议据此做数据库脚本。表名表用途关键字段说明sys_user用户表id, username, password, role, phone, nickname, avatar, create_timehouse房源表id, landlord_id, title, address, region, price, area, bedroom, hall, imgs, status, audit_status, create_timelease合同表id, house_id, tenant_id, landlord_id, start_date, end_date, rent_month, deposit, status, create_timebill账单表id, lease_id, type, amount, status, deadline, pay_timerepair报修表id, house_id, user_id, content, status, feedback, create_time, handle_timefavorite收藏表id, user_id, house_id, create_time6张表不是拍脑袋定的每张表都对应一个明确的业务动作。sys_user承载登录和角色区分house承载租赁的物lease把人和物关联起来bill承载资金流水repair承载售后服务闭环favorite承载租客的前期意向。你在设计自己的系统时也遵循这个原则每个核心业务动作最好都是一张独立的表不要揉在一起。2.2 关键表的设计细节和字段取舍先看house表这是整个系统里信息量最大的表。除了常规的title、address、price、area之外有几个字段值得特别注意audit_status这是房源的审核状态用0/1区分待审核和已通过。很多人会把审核状态和上架状态合并成一个status字段看起来省事但实际运营中管理员可能需要先审核、再上架两步操作拆开才能在页面上做待审核列表和已上架列表两个视图。imgs房源图片字段我用JSON字符串存储多张图片路径这样实现图片轮播很简单。如果你用MySQL 5.7以下版本可以用逗号分隔但处理起来稍麻烦。region区域字段单独拎出来方便后续做区域筛选。虽然也可以从address里模糊匹配但查询效率低而且区域数据不干净的时候筛选结果会很尴尬。lease表是另一个重点。租期字段用start_date和end_date精确到天不要只存一个租期数字因为系统要根据这两个字段自动计算账单和判断合同状态。status字段建议用int类型规则约定1待确认、2生效中、3已到期、4已退租。在我的实现里0不需要因为合同从创建的瞬间就要进入1状态。bill表的type字段区分租金和水电费amount字段直接存金额不要存单价和数量再计算因为水电费计算规则随时可能改存最终金额才能保证历史账单不被规则变更影响。提示建表时统一使用InnoDB引擎和utf8mb4字符集。InnoDB保证事务和行级锁utf8mb4保证用户输入的任何特殊字符emoji表情都不会报错踩过这个坑的都懂。2.3 外键和索引怎么处理很多学生项目喜欢建表时顺手把外键加上但在实际项目里我建议别加物理外键而是用代码保证逻辑关联。为什么因为物理外键会让数据迁移、批量删除变得非常痛苦而且性能上有额外开销。这6张表本身业务关联不太复杂逻辑外键足够了。索引的取舍上几个高频查询字段必须加索引ALTER TABLE house ADD INDEX idx_h_landlord (landlord_id); ALTER TABLE house ADD INDEX idx_h_status (status, audit_status); ALTER TABLE lease ADD INDEX idx_l_tenant (tenant_id); ALTER TABLE lease ADD INDEX idx_l_house (house_id);这是我从一个系统越跑越慢的教训里提炼出来的数据量没到几十万条之前单表查询加索引后的性能表现完全够用不需要过早引入缓存或分库分表。3. 后端核心模块实现SpringBoot MyBatis的关键代码3.1 三层架构与接口规划后端按照controller - service - mapper三层去拆。Controller只负责参数接收和结果返回Service里写业务逻辑Mapper只管数据库读写。这个分层看起来基础但真的能保证项目不乱——尤其是当功能越来越多时没有分层就像把所有衣服堆在一个衣柜里。核心接口规划如下模块接口功能说明用户模块POST /api/user/login登录返回token用户模块POST /api/user/register注册默认角色租客房源模块GET /api/house/list条件分页查询房源房源模块POST /api/house/add房东新增房源房源模块GET /api/house/auditList管理员查看待审核列表房源模块POST /api/house/audit管理员审核房源合同模块POST /api/lease/apply租客发起租赁申请合同模块POST /api/lease/handle房东处理申请合同模块POST /api/lease/renew续约账单模块GET /api/bill/list查看个人账单账单模块POST /api/bill/pay账单缴费报修模块POST /api/repair/add提交报修登录我用了JWTJSON Web Token而不是Session因为前后端分离的项目里前端Vue和后端SpringBoot很可能部署在不同的端口甚至服务器Session的跨域处理要配置很多东西JWT天然无状态前端把token存到localStorage里请求时带上Authorization头即可。3.2 登录接口和JWT工具类的实现JWT的核心逻辑不复杂生成token时把用户id和角色放进去后续每个请求都通过拦截器解析token拿到当前用户信息。下面这段是生成token的工具方法public String generateToken(Long userId, String role) { Calendar calendar Calendar.getInstance(); calendar.add(Calendar.HOUR, 24); // token过期时间24小时 Date expireDate calendar.getTime(); return Jwts.builder() .setSubject(userId.toString()) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }登录接口的逻辑是前端传username和password - 后端查用户 - 密码用MD5加密后比对真实项目建议升级为BCrypt- 成功则返回token和用户信息。要注意密码绝对不能在日志里打印我调试时吃过一次亏把日志文件给了别人里面明文密码全暴露了。拦截器是JWT方案里的关键一环public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token.substring(7)) .getBody(); request.setAttribute(userId, Long.parseLong(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } }这里我特别说明一下为什么要用request.setAttribute把userId存起来而不是每次在Service里重新解析token。因为一次请求链路里Controller和Service可能多次需要当前用户信息每次都解析浪费时间一次解析存到request域后续直接取就行。这是一个很小的性能优化但在高并发接口里体现很明显。3.3 MyBatis动态SQL多条件房源查询房源列表页是最典型的动态SQL场景用户可以选择区域、价格区间、面积范围还能按发布时间排序。如果用静态SQL光是应付条件组合就要写七八条SQL语句维护成本很高。MyBatis的where和if标签完美解决了这个问题。select idselectHouseList resultTypecom.example.entity.House SELECT h.*, u.nickname AS landlord_name FROM house h LEFT JOIN sys_user u ON h.landlord_id u.id where if testregion ! null and region ! AND h.region #{region} /if if testminPrice ! null AND h.price gt; #{minPrice} /if if testmaxPrice ! null AND h.price lt; #{maxPrice} /if if testarea ! null AND h.area gt; #{area} /if if testkeyword ! null and keyword ! AND (h.title LIKE CONCAT(%, #{keyword}, %) OR h.address LIKE CONCAT(%, #{keyword}, %)) /if AND h.audit_status 1 AND h.status 1 /where ORDER BY h.create_time DESC /select有几个细节是新手容易踩坑的第一gt;和lt;是XML转义符不能直接写和否则XML解析直接报错。第二CONCAT拼接模糊查询比直接写%${keyword}%安全得多因为${}是字符串替换存在SQL注入风险而#{}是预编译占位符。第三where标签会自动去掉第一个多余的AND但如果条件全为空它不会生成WHERE关键字这是一个非常优雅的特性。分页我用的是PageHelper插件用法很简单PageHelper.startPage(pageNum, pageSize); ListHouse list houseMapper.selectHouseList(query); PageInfoHouse pageInfo new PageInfo(list);PageHelper通过拦截器在SQL执行前自动拼接LIMIT返回值里自带total、pageNum、pageSize等分页数据前端做分页组件时直接取total就行。3.4 事务处理租赁申请和账单生成的一致性租赁申请是整个系统里最需要事务保障的业务。租客提交申请后系统要做三件事创建合同记录、更新房源状态为已申请、生成首期账单。这三个操作要么全成功要么全失败只成功一半就会出现合同已建但房源还能被其他人申请的脏数据。SpringBoot处理事务非常方便在Service方法上加Transactional注解即可Transactional(rollbackFor Exception.class) public Integer applyLease(LeaseApplyRequest request) { // 1. 校验房源状态防止并发重复申请 House house houseMapper.selectById(request.getHouseId()); if (house null || house.getStatus() ! 1) { throw new RuntimeException(房源不存在或不可申请); } // 2. 创建合同 Lease lease new Lease(); lease.setHouseId(request.getHouseId()); lease.setTenantId(request.getTenantId()); lease.setLandlordId(house.getLandlordId()); lease.setStartDate(request.getStartDate()); lease.setEndDate(request.getEndDate()); lease.setRentMonth(house.getPrice()); lease.setDeposit(house.getDeposit()); lease.setStatus(1); leaseMapper.insert(lease); // 3. 更新房源状态 houseMapper.updateStatus(request.getHouseId(), 2); // 4. 生成首期账单 Bill bill new Bill(); bill.setLeaseId(lease.getId()); bill.setType(1); // 1-租金 bill.setAmount(house.getPrice()); bill.setStatus(0); // 0-待支付 billMapper.insert(bill); return lease.getId(); }一个容易忽略的点是rollbackFor Exception.class。如果不指定这个参数Spring默认只在RuntimeException时回滚检查异常比如FileNotFoundException不会触发回滚。实际业务里很多异常是自定义的运行时异常但为了保险起见建议一律显式指定。注意事务方法里不要catch掉异常然后又吞掉否则事务根本不会感知到错误也就不会回滚。正确做法是让异常抛出去或者catch后手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。4. 前端Vue实现从页面搭建到接口联调4.1 Vue项目的目录结构前端这块我用的是Vue2 Element UI虽然不是最新的Vue3但胜在生态成熟稳定对管理系统来说组件库丰富、资料多、上手快。如果你没有历史包袱直接上Vue3 Element Plus也是可以的核心逻辑差别不大。前端目录结构按模块划分src/ api/ // 所有接口请求封装 user.js house.js lease.js bill.js router/ // 路由配置 index.js store/ // Vuex状态管理 index.js views/ login/ // 登录注册页 home/ // 首页房源列表 house/ // 房源详情 admin/ // 管理后台 tenant/ // 租客个人中心 landlord/ // 房东工作台 components/ // 公共组件 HouseCard.vue我建议所有接口请求统一放在api目录里封装比如house.js里导出所有房源相关接口函数。这样页面组件里不直接出现axios调用后期接口地址变了只需要改api目录全局搜索一下就好了。4.2 路由守卫实现登录校验Vue前端做登录拦截的核心是路由守卫。前端这边的拦截逻辑要和后端配合没有token就跳转登录页有token但访问了无权限的页面也要拦截。以下是我的实现思路router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); return; } if (!token) { next(/login); return; } // 角色权限控制 const role localStorage.getItem(role); if (to.meta.roles !to.meta.roles.includes(role)) { // 没有权限访问跳转到首页 next(/); return; } next(); });to.meta.roles在路由配置里定义比如管理后台的路由加上roles: [admin]房东工作台路由加上roles: [landlord]。这套前端路由守卫配合后端拦截器实现了双层防护。我强调一下前端路由守卫只是改善用户体验的真正的安全防线必须靠后端前端绕过路由守卫太简单了。4.3 房源列表页的筛选和分页实现房源列表页是租客看到的第一屏体验好坏直接影响整个项目印象。我用的方案是搜索表单 房源卡片列表 分页组件。搜索表单绑定一个查询对象提交时调用API接口searchParams() { return { region: this.region, minPrice: this.minPrice, maxPrice: this.maxPrice, keyword: this.keyword, pageNum: this.currentPage, pageSize: 8 }; }, loadHouseList() { getHouseList(this.searchParams).then(res { this.houseList res.data.list; this.total res.data.total; }); }这里有一个经验前端传给后端的参数名要能直接映射到MyBatis的Mapper参数里减少一层参数转换。比如前端字段是minPriceMapper里也是minPrice就不用再写一堆map.put(minPrice, xxx)。房源卡片组件里展示图片、标题、价格、面积、区域点击跳转到详情页。详情页再展示大图轮播、户型描述、联系房东按钮、申请租房按钮等。4.4 管理后台的表格页面管理后台是我觉得这套系统里最省力的部分因为Element UI的el-tableel-paginationel-dialog三件套基本就能搞定所有页面。用户管理、房源审核、账单列表本质都是同一个模式表格展示数据 搜索 操作按钮通过/拒绝。以房源审核为例PageHelper返回的total直接绑到el-pagination上按钮点击调审核接口handleAudit(row, status) { auditHouse({ id: row.id, auditStatus: status}).then(() { this.$message.success(status 1 ? 审核通过 : 已拒绝); this.loadAuditList(); }); }这里需要注意审核操作的幂等性后端接口里审核前要判断房源当前状态如果已经是已审核状态就不能再次审核否则会出现重复操作。前端也要在点击后立即禁用按钮防止用户快速双击导致重复请求。4.5 axios请求封装axios封装是个绕不开的基础工作。我习惯把所有请求的公共逻辑抽到一个request工具类里包括baseURL设置、请求头带token、响应统一处理错误码。service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { this.$message.error(res.message); if (res.code 401) { localStorage.clear(); router.push(/login); } return Promise.reject(new Error(res.message)); } return res; }, error { if (error.response error.response.status 401) { localStorage.clear(); router.push(/login); } return Promise.reject(error); } );后端统一返回格式{code: 200, message: success, data: ...}前端在响应拦截器里把code判断抽出来所有页面里的then回调拿到的直接就是data数据不用每个页面都判断code。同时在后端拦截器返回401时前端自动清除本地token并跳转登录页这是处理登录过期最省心的方案。5. 部署上线SpringBoot打包和Vue构建5.1 SpringBoot项目打包项目完成后要部署第一步是把后端打成jar包。SpringBoot的打包非常简单但有几个配置细节容易忽略。先在pom.xml里确认打包插件build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build然后在application.yml里把数据库连接、端口等配置放到环境对应的profile里。我习惯区分application-dev.yml本地开发和application-prod.yml生产环境启动时通过--spring.profiles.activeprod切换。打包命令mvn clean package -DskipTests打出来的jar包在target目录下直接运行java -jar house-lease-system.jar --spring.profiles.activeprod这里我强调一个部署细节生产环境的MySQL密码不要明文写在配置文件里至少也应使用环境变量占位spring: datasource: url: jdbc:mysql://localhost:3306/house_lease?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: ${DB_USERNAME} password: ${DB_PASSWORD}启动时通过环境变量传入DB_USERNAMEroot DB_PASSWORDyourpassword java -jar house-lease-system.jar5.2 Vite打包和SpringBoot整合前端打包分两种情况一种是前后端完全分离部署前端打包出的静态文件扔到Nginx里配合后端接口反向代理另一种是把前端打包后的dist目录直接放进SpringBoot的src/main/resources/static下让SpringBoot同时提供静态资源服务。对于中小型项目第二种方案要省事得多。先执行前端打包npm run build打包完成后dist目录拷到SpringBoot项目的resources/static下重启后端就内外网都能访问了。有几个注意点前端接口的baseURL不能写死成localhost要写成相对路径或者和后端相同域名的API路径。比如由Nginx转发的写/api就好。检查dist里的index.html引用的静态资源路径是相对路径./而不是绝对路径/不然部署到二级路径会白屏。5.3 MySQL部署和数据导入生产环境的MySQL安装我踩过不少坑。在Linux上安装MySQL最稳的方式是使用官方yum仓库或者dockerdocker run -d --name mysql8 -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyourpassword \ -e MYSQL_DATABASEhouse_lease \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0用docker部署MySQL的优点是隔离干净、备份方便数据目录挂载在宿主机上容器随便删了重建数据都不丢。这个方案我在多个项目上实测比裸机rpm安装省心太多。建库建表后执行初始化数据脚本把管理员账号init进去。管理员账号密码建议用BCrypt加密后存储不要用明文。你可以写一个简单的初始化类在SpringBoot启动时检测到user表为空就自动插入管理员账号这样交付时省去手动操作的麻烦。6. 常见问题与排查技巧实录6.1 跨域问题前后端分离开发时最常见的第一个报错就是跨域。前端访问后端接口浏览器报CORS错误。解决方式在后端加一个CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }要注意的是allowCredentials(true)必须和allowedOriginPatterns(*)配合不能单独用allowedOrigins(*)否则JDK高版本会报错——这是SpringBoot 2.4之后的严格校验。另外配置了JWT拦截器之后预检请求OPTIONS也要直接放行否则前端正式请求之前就挂在预检上了。处理方法是拦截器里判断请求方法OPTIONS请求直接返回true。6.2 MyBatis日志不打印SQL调试阶段最需要看的就是MyBatis执行的SQL语句方便定位问题。在application.yml里加mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这个配置会把每条SQL及参数值直接打印到控制台是排查SQL问题的首选工具。我排查一个联表查询查不出数据的问题时就是靠看打印出来的SQL发现在ON条件里顺手加了一个错误的过滤条件导致结果集为空。定位效率非常高。6.3 数据库连接报SSL错误MySQL 8.0以上版本默认开启SSL连接时如果没配置会报SSL connection error: The server requested SSL, but the client did not provide it.解决办法是在JDBC URL里添加url: jdbc:mysql://localhost:3306/house_lease?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueallowPublicKeyRetrievaltrue这个参数也很重要MySQL 8.0使用caching_sha2_password认证插件时没这个参数也会连接失败。这两个参数属于高频踩坑点写进标准配置里就避免了后续反复折腾。6.4 前端npm install报错Vue项目clone下来后执行npm install成功但启动报错大概率是Node版本和依赖的兼容性问题。Element UI 2.x版本在Node 18以上会有启动警告和奇怪报错。我的建议是固定开发环境的Node版本我长期用的方案是Node 16.14搭配Vue2项目的兼容性最稳。也可以装nvm管理Node版本随时切换。6.5 后端启动端口占用SpringBoot默认端口8080如果本地已经被其他Java进程占用启动会直接报端口冲突。常规做法是在application.yml里改一个端口但我更推荐用--server.port参数动态指定端口java -jar house-lease-system.jar --server.port8081这样本地开多个服务互不干扰尤其调试多个项目并存的场景下很实用。写在最后的几点个人体会这些功能全部做完之后我回过头来看整个项目最大的体会是这种业务管理系统的技术难度不在于哪个技术点特别深而在于把每个环节稳稳当当地串起来。从前端表单的字段校验到后端接口的参数校验再到数据表字段的合理性任何一环出现疏漏最终都会以用户看得见的问题反馈出来。这套系统我实际迭代了三轮才稳定下来。第一轮实现了基础CRUD第二轮补充了审核流程和权限控制第三轮优化了账单生成逻辑和数据统计。每一轮改动都有明确的目标没有盲目堆功能。如果你打算做类似的系统我也建议你有节奏地迭代不要想着一次做到尽善尽美。另外再分享一个小技巧像这种SpringBoot Vue的管理系统最后交付时建议配一个自启动的运维脚本把MySQL、后端、前端服务的启动命令写进去这样换一台新机器部署时不用一条一条命令敲省下大量时间。脚本本身不复杂十几行bash脚本的事但客户体验完全是两个档次。
返回列表