ARTICLE DETAIL

资讯详情

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

企业级房屋租赁管理系统实战:SpringBoot+Vue+MyBatis+MySQL全解析

企业级房屋租赁管理系统实战:SpringBoot+Vue+MyBatis+MySQL全解析 这篇只有正式开始开工之前先来聊个稍微有点扎心的事实房屋租赁这块业务看起来无非就是“房源 - 租客 - 合同 - 收费”四个词但真到落地时候管理混乱的点比你想象的要多得多。纸质合同到期没人提醒、水电费抄表全靠微信聊、押金退还扯不清、空置房源底下一堆人不知道谁在跟进……我见过不少房东和管理公司的所谓“管理系统”其实就是一张Excel表稍微正规点的用了飞书多维表格但权限、账单、合同、押金这些真正要紧的流程没有串起来最后还是靠人肉盯。所以SpringBoot Vue MyBatis MySQL这个组合做的企业级房屋租赁管理系统并不是什么花哨的技术炫耀。它的核心价值就一句话通过一套完整源码把“房源管理、客户跟进、合同签约、账单生成、财务核销、到期提醒、角色权限”这几条线全部用代码串起来让租赁业务从“人管”变成“系统管”。这篇文章我会从业务建模、技术选型、数据库设计、核心功能实现到踩坑记录完整拆解这套系统。适合谁看想拿企业级项目练手提升开发经验的Java中学生想自建管理平台的中小租赁公司、公寓运营方也包括那些需要给客户定制租赁系统的外包选手——你们要的细节这里都有。1. 系统整体设计与业务架构拆解1.1 从业务痛点倒推出来的功能边界我参与过不少租赁管理项目的早期规划发现一个规律需求方嘴上说要“管理系统”实际要的东西往往很集中逃不出下面这五条线房源底账小区、楼栋、房间、户型、朝向、面积、配置设施、产权信息这是整个系统的地基。没有一套清晰的房源台账后面谈什么合同和财务都是扯淡。客户与租约租客从看房、跟进、签约到到期退租整个生命周期内涉及的状态切换。账单与财务月租金、押金、水电费、物业费、违约金每一个收费项都得有独立的账单记录和核销状态。租务提醒合同到期前30天怎么提醒房租账单逾期了怎么催需要系统具备自动化的状态感知能力。权限与审计财务可以看到所有账单金额管家只能看自己负责的楼栋再灵活一点还要支持权限到数据组维度。这五条线就决定了这个系统的功能边界。按照SpringBoot Vue前后端分离的方式来拆后端按模块分包前端按页面组织分工就很清晰了。1.2 角色权限不搞复杂的RBAC但必须够用市面上很多教程一说到权限就是RBAC五张表功能做得很重实际项目里如果只有固定几个角色反而建议更务实一点。这套系统里我建议用的方案是角色表固定内置四种超级管理员、财务、管家运营、租客。权限控制分两层菜单权限通过路由动态生成数据权限通过查询条件里的OrganizationId/DepartmentId字段过滤。从实现来讲后端就是Spring Security或者Sa-Token做认证授权。Spring Security在SpringBoot体系里集成最自然Sa-Token学习曲线更平缓。对这套源码这种业务系统我推荐Sa-Token它的登录、权限注解、踢人下线、Redis集成都做得比较省事企业项目里用起来很顺手。如果你想展示自己“很专业”Spring Security JWT Redis做token管理也可以但是代码量会明显多出去一大截。数据权限这块经常被忽略但实际运营场景很关键。比如A管家只管A栋那B栋的房源数据就不应该出现在他的列表里。实现方式不复杂——在Mapper的查询SQL里拼一个动态条件AND department_id #{currentUser.departmentId}。每个查询都要带上漏一个就是一个越权漏洞。1.3 核心业务流程签约和退租的状态流转这个系统里最核心的两个业务流程是签约和退租也是我在设计时花时间最多的部分。签约流程可以简化为五步管家在“房源管理”里选择一套空置房源标记为“洽谈中”。录入租客信息创建租客档案。生成合同草稿填写起租日期、月租金、押金方式、付款周期。合同确认后系统同时做三件事合同状态变为“履约中”房源变为“已出租”生成首期租金账单。租客完成首笔支付财务核销账单。第4步是非常经典的“多表联动”场景必须用Transactional。我在之前的项目里见过很多新手在这一步出问题——合同建好了房源状态没变导致一个房间被两次签约或者是房租账单没生成财务那边清单对不上。用事务把所有写操作包在一起任何一个环节报错都整体回滚才能保证数据不错乱。退租流程则要处理合同结束、押金计算含水电扣款、房屋损毁扣款、违约情况、房源恢复空置、在租租客标签清理。这一串操作里最容易被忽略的是水电费的滞后性——退租当天抄完表才能结算所以退租流程最好支持“先结算后完成”也就是退租单挂在“待结算”状态等财务录入水电读数后再真正收官。2. 技术栈选型与技术要点解析2.1 SpringBoot为什么它是后端底座的不二选择用SpringBoot做企业级系统的后端底座在今天已经不需要过多论证。它自动装配、独立运行、生态成熟一个spring-boot-starter-web就把内置Tomcat带起来了开发者不需要去折腾WAR包部署一个java -jar就能跑。但这套房屋租赁系统用SpringBoot不光是图省事。更关键的原因是它和Spring家族其他组件Spring Security、Spring Data Redis、Spring Task、Spring Validation的协作非常顺。比如你后面要加一个定时任务——每日扫描合同到期提醒、账单逾期催缴代码就是几个Scheduled注解标注的方法配一下cron表达式就完事。项目从一开始就有这种扩展空间是SpringBoot这类生态型框架最大的优势。版本选择上有句实在话如果是自己学习或二次开发建议用SpringBoot 2.7.x而不急着上3.x。为什么3.x把javax改成jakarta命名空间很多老版本的MyBatis starter、第三方工具类都要跟着升级网上教程大部分还是2.x的写法踩到版本坑的概率大很多。对于“先跑通、再升级”的项目逻辑2.7是最稳的起点。2.2 Vue前端组件化与权限路由的实现前端部分用Vue核心不是页面多好看而是数据和视图的双向绑定、组件复用和路由行为可控。房屋租赁系统的前端页面量其实不小后台有房源管理、合同管理、账单管理、数据看板租客端还有在线缴费、报修申请。如果全部用jQuery手写光这些页面的DOM操作就能写到崩溃用Vue的组件化不同的业务模块拆成独立的.vue单文件组件维护成本低一个量级。这里要重点提一下动态路由。这套系统不同角色进来的菜单应该不一样租客端看到的是“我的合同、我要缴费、我的报修”后台管家看到的是“房源、租客、合同”。实现逻辑很简单用户登录成功后后端返回该用户的角色码和菜单权限列表。前端拿到权限数据后通过router.addRoute()动态添加对应的路由记录。vue-router的守卫里再判断一下未登录状态和越权访问直接重定向到404或者登录页。模板理解起来简单落地的时候有个细节容易翻车刷新页面时addRoute的路由会丢失因为Vue实例是重新创建的。解决方案也很常规——把权限信息存到localStorage或者Pinia刷新后先根据缓存重新构建路由再决定放行到目标页面。Vue版本的选择如果是新项目直接用Vue3 Vite。它的组合式API在写复杂业务页面时更灵活。但如果拿到的这套源码是基于Vue2 Element UI别急着推倒重来先跑通再渐进式迁移企业项目里“稳定优先”比“技术最新”重要得多。2.3 MyBatisSQL控制力是关键诉求选MyBatis而不是JPA/Hibernate我对理由一直很明确房屋租赁系统的查询场景非常依赖精细化SQL控制而MyBatis把SQL完全交到开发者手里复杂多表联查、动态条件拼接、批量更新都很好掌控。举一个实际例子房源列表页的高级筛选楼层、朝向、户型、租金区间、状态、所属小区这六个条件可能任意组合为空。用MyBatis的动态SQL写就是select idselectRoomPage resultTypecom.lease.entity.RoomVO SELECT r.id, r.room_no, r.floor, r.area, r.status, b.name AS building_name FROM room r LEFT JOIN building b ON r.building_id b.id where if testfloor ! null and floor ! AND r.floor #{floor} /if if testorientation ! null and orientation ! AND r.orientation #{orientation} /if if testminRent ! null AND r.monthly_rent gt; #{minRent} /if if testmaxRent ! null AND r.monthly_rent lt; #{maxRent} /if if teststatus ! null and status ! AND r.status #{status} /if if testbuildingName ! null and buildingName ! AND b.name LIKE CONCAT(%, #{buildingName}, %) /if /where ORDER BY r.updated_time DESC LIMIT #{offset}, #{pageSize} /select这套逻辑用JPA写的话要么拼Specification要么用Query里的JPQL表达力都不如原生SQL直白。而且加了where标签后MyBatis会自动处理AND前缀问题避免XML里常见的“WHERE后面多一个AND”的坑。So把它理解成一个原则Java对象负责接受参数、封装结果SQL源动力永远掌握在开发者的手里。这套管理系统里凡是涉及多表单表查询的画风都差不多。2.4 MySQL事务与数据一致性数据库选MySQL不必赘述。InnoDB存储引擎支持行级锁、事务、崩溃恢复企业级应用默认选择没有之一。这个系统里要特别重视两件事。第一件是字符集和排序规则创建库时就要指定否则后期改起来特别痛苦CREATE DATABASE lease_manage DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;为什么是utf8mb4而不是utf8因为MySQL的utf8编码最大只支持3个字节存储不了生僻字和emoji表情。租客姓名里万一有生僻字或者住户备注里填了表情符号直接用utf8就会报错或乱码utf8mb4才是完整的UTF-8。第二件是事务隔离级别。系统里涉及大量“读后写”场景最典型的是签约时判断房间是否被占用SELECT status FROM room WHERE id #{roomId} -- 然后在代码里判断 status EMPTY 才继续插入合同这里的判断和后续的UPDATE之间如果有并发操作房间就可能被重复签约。解决的办法不只是升级隔离级别到SERIALIZABLE就完事更合理的做法是直接对房间记录加行锁SELECT status FROM room WHERE id #{roomId} FOR UPDATE用SELECT FOR UPDATE先把这行锁住再执行状态更新和合同插入串行化处理掉这个并发风险点。这就是为什么系统里核心的签约业务都要单独设计一套“防并发”的流程控制而不是简简单单一个Insert。3. 数据库设计核心细节与关键表结构3.1 房源建模小区-楼栋-房间三层结构房源这块建议分三层模型小区Community、楼栋Building、房间Room。直接一张房间表带所有冗余字段也能跑但后续统计“X小区每平米均价”“X栋出租率”的时候你要么写一堆重复GROUP BY要么就只能重新加字段返工。规范的三层模型一劳永逸而且很贴合真实楼盘的组织方式。关键表结构设计CREATE TABLE building ( id BIGINT PRIMARY KEY AUTO_INCREMENT, community_id BIGINT NOT NULL COMMENT 所属小区ID, building_no VARCHAR(50) NOT NULL COMMENT 楼栋号, total_floors INT DEFAULT 1, elevator_count INT DEFAULT 0, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_community_building (community_id, building_no) ) ENGINEInnoDB COMMENT楼栋信息表;CREATE TABLE room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_id BIGINT NOT NULL, room_no VARCHAR(50) NOT NULL, floor INT NOT NULL, area DECIMAL(10,2) NOT NULL, orientation VARCHAR(20) COMMENT 朝向: 东/南/西/北/南北, monthly_rent DECIMAL(10,2) NOT NULL, deposit_months INT DEFAULT 1 COMMENT 押几个月, status VARCHAR(20) DEFAULT EMPTY COMMENT EMPTY空置/RENTED已出租/LOCKED锁定/REPAIR维修中, room_type VARCHAR(20) COMMENT 主卧/次卧/整租/开间, amenities VARCHAR(500) COMMENT 配套设施逗号分隔, remark VARCHAR(500), UNIQUE KEY uk_building_room (building_id, room_no), KEY idx_status (status), KEY idx_rent (monthly_rent) ) ENGINEInnoDB COMMENT房间信息表;这里status字段是房间状态机一定要在代码中用枚举类控制取值范围不要直接塞字符串。它的状态流转只能按照固定方向EMPTY - LOCKED - RENTED - EMPTY中间可以插一个REPAIR。状态机的设计让整个系统逻辑清晰很多比如房源列表里默认只能看到EMPTY和LOCKED的房间可操作签约RENTED的房间就只能看到租约详情。3.2 合同表最核心的业务单据合同表是整张租赁系统的数据枢纽几乎所有业务都要和它发生关联。字段设计上我特别强调几点CREATE TABLE lease_contract ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_no VARCHAR(64) NOT NULL COMMENT 合同编号需唯一, room_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, landlord_id BIGINT COMMENT 业主ID托管场景使用, monthly_rent DECIMAL(10,2) NOT NULL, deposit_amount DECIMAL(10,2) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, rent_due_day INT DEFAULT 1 COMMENT 每月几号交租, payment_cycle VARCHAR(20) DEFAULT MONTHLY COMMENT 月付/季付/半年付/年付, status VARCHAR(20) DEFAULT PENDING COMMENT PENDING待生效/ACTIVE履约中/EXPIRED已到期/TERMINATED已终止, sign_time DATETIME, create_by BIGINT COMMENT 创建人管家ID, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_contract_no (contract_no), KEY idx_room_id (room_id), KEY idx_tenant_id (tenant_id), KEY idx_date (start_date, end_date) ) ENGINEInnoDB COMMENT租约合同表;有一个细节容易被新手忽略房租的“每月的几号交租”单独抽象成rent_due_day字段不要hidden在end_date里推导。因为租客可能1号签订合同、5号入住、约定每月10号交租这些日期是独立的。后面生成缴纳提醒时逻辑就是“每月X号扫描一次所有ACTIVE状态的合同给租金到期未缴的租客发提醒”一天的延迟都可能引起租客投诉设计字段时要为这种业务逻辑铺路。3.3 账单表费用科目和核销状态租赁系统的财务核心是账单。房租、押金、水电费、物业费、违约金都统一走账单表然后靠bill_type区分CREATE TABLE rent_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(64) NOT NULL, contract_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, bill_type VARCHAR(20) NOT NULL COMMENT RENT房租/DEPOSIT押金/WATER水费/ELECTRIC电费/PROPERTY物业费/PENALTY违约金, bill_period VARCHAR(20) COMMENT 账单周期如2024-03, amount DECIMAL(10,2) NOT NULL, paid_amount DECIMAL(10,2) DEFAULT 0, status VARCHAR(20) DEFAULT UNPAID COMMENT UNPAID未支付/PARTIAL部分支付/PAID已支付/OVERDUE逾期/CANCELLED作废, due_date DATE NOT NULL, pay_time DATETIME, remark VARCHAR(500), KEY idx_contract (contract_id), KEY idx_tenant (tenant_id), KEY idx_status_due_date (status, due_date) ) ENGINEInnoDB COMMENT账单表;设计里有两点要说明。第一部分支付这种场景一定要支持现实中租客“先转一半、下个月补另一半”太常见了账单表里同时有amount和paid_amount核销时累加paid_amount即可。第二状态机里单列一个OVERDUE而不是由状态推导因为逾期虽然本质上是“该付没付”但系统定时扫描时应该在短时间内批量把“超过due_date且未支付”的账单状态改成OVERDUE这样才能配合催缴任务去触发短信/站内信。状态字段不仅仅是描述还是后面定时任务的一种状态标记。3.4 索引设计查询场景反推索引我见过很多开发者的表结构联合索引乱加一气最后写入性能被拖垮。这个系统的查询场景其实非常清晰核心就是“到期管理”和“账单催收”查明天哪些合同到期idx_date (start_date, end_date)。查哪些账单逾期idx_status_due_date (status, due_date)因为经常是WHERE statusUNPAID AND due_date NOW()。查房源列表按租金区间过滤idx_rent (monthly_rent)。查某栋楼某房间的唯一性uk_building_room (building_id, room_no)。记住一个原则索引不是越多越好而是要针对最频繁的查询组合建。很多人一说到优化就是“慢SQL加索引”但真正的索引设计应该在表结构设计阶段就按查询场景规划好。4. 核心功能模块实操实现4.1 登录鉴权用Sa-Token还是Spring Security如果你直接从这套源码入手开发登录模块是最先需要搭建的。我强烈建议就算你最终决定用Spring Security第一版也用Sa-Token或者其他轻量框架跑通流程因为业务系统真正难的从来不是安全框架本身而是“用户状态管理”和“会话控制”。用Sa-Token的话登录核心逻辑只有三步PostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 1. 根据用户名查用户比对密码BCrypt加密 SysUser user userService.getByUsername(dto.getUsername()); if (user null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } // 2. 登录Sa-Token自动生成token StpUtil.login(user.getId()); // 3. 返回token和用户基本信息 return Result.success(new LoginVO( StpUtil.getTokenValue(), user.getRealName(), user.getRoleCode() )); }为什么要用BCrypt而不是MD5因为MD5已经被彩虹表破解得七七八八而且不加盐的MD5看起来和明文没啥区别。BCrypt自动附带随机盐同一密码两次加密结果都不同安全性完全不是一个层次。企业级系统要求审计和防拖库密码这关绝对不能糊弄。密码盐值逻辑讲到这儿给你提个醒千万不要自己实现一套加盐逻辑容易越写越错直接用现成的BCrypt就好。4.2 房源管理多条件检索与状态联动房源列表页是前端交互最重的模块缩略图、筛选器、分页、批量操作全堆一起。而后端实现的核心是前面那张动态SQL查询。这里要补充的是“新增房源”这个看似简单的功能背后的一串关联操作Transactional public void addRoom(Room room, Long buildingId) { // 1. 校验楼栋是否存在 Building building buildingMapper.selectById(buildingId); Assert.notNull(building, 楼栋不存在); // 2. 校验同楼栋房间号唯一 Room exist roomMapper.selectByBuildingAndNo(buildingId, room.getRoomNo()); Assert.isNull(exist, 该楼栋下房间号已存在); // 3. 插入房间 room.setBuildingId(buildingId); room.setStatus(RoomStatus.EMPTY.getCode()); roomMapper.insert(room); // 4. 更新楼栋的房间数量统计 buildingMapper.incrRoomCount(buildingId); }看到没有就一个“新增房间”触发了两张表更新。如果不在事务里中间一旦出错楼栋统计和房间数据就对不上。这种事务边界一定要养成肌肉记忆。这里我说的状态联动包括一个很实用的“附近同户型参考租金”实现方法就是在查询房间详情时把同楼栋同户型、当前为空置状态的几条记录也查出来成本很低但管家在定价的时候会非常受用。4.3 签约事务一个操作联动三张表签约是整个系统里最复杂的一个事务前面已经提过一次这里把关键代码的骨架拉出来看看Transactional public LeaseContract signContract(SignRequest req) { // 1. 查房间并加锁 Room room roomMapper.selectByIdForUpdate(req.getRoomId()); Assert.isTrue(RoomStatus.EMPTY.equals(room.getStatus()), 房间已被占用或锁定); // 2. 创建租客档案如已存在则直接关联 Tenant tenant tenantMapper.selectByIdCard(req.getIdCard()); if (tenant null) { tenant new Tenant(); tenant.setName(req.getTenantName()); tenant.setIdCard(req.getIdCard()); tenant.setPhone(req.getTenantPhone()); tenantMapper.insert(tenant); } // 3. 创建合同记录初始状态为PENDING LeaseContract contract buildContract(req, tenant.getId()); contractMapper.insert(contract); // 4. 房间状态改成LOCKED防止其他人重复操作 roomMapper.updateStatus(room.getId(), RoomStatus.LOCKED); // 5. 生成首期账单押金首月房租 generateFirstBills(contract); return contract; }这段代码里SELECT FOR UPDATE解决并发问题Transactional保证多条SQL要么同时成功要么整体回滚而把房间状态从EMPTY改成LOCKED是为了防止“合同草稿期间被其他管家抢签”。这套流程走顺了后面开发退租、换房、续签这些操作时只需要在它的基础上调整状态流转就好。有个细节合同编号建议在创建时用“前缀日期流水”方式生成比如HT202403120001前端显示好看财务对账时也能一眼看出是哪天录的合同。直接用数据库自增ID当合同编号虽然省事但合同打印出来被人一眼看到数据量这种细节早晚会被运营吐槽。4.4 账单生成与缴费核销账单生成有两种方式我在系统里是同时用的签约时生成首期房租、押金在签约事务里直接生成。定时任务批量生成续期账单由Spring Task每天扫描一遍ACTIVE状态合同判断是否到达下一个计费周期的生成时间。后者的核心代码大概长这样Component public class RentBillGenerateTask { Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void generateMonthlyBills() { ListLeaseContract activeContracts contractMapper.selectActiveContracts(); for (LeaseContract contract : activeContracts) { // 判断当前月份账单是否已生成 RentBill exist billMapper.selectByContractAndPeriod(contract.getId(), currentMonth()); if (exist null) { billMapper.insert( buildRentBill(contract, currentMonth()) ); } } } }这里每个月2点跑是防止把当天刚签约的新合同遗漏。生产环境里一定要加分布式锁或者任务幂等设计不然集群部署时多个实例同时跑就会生成重复账单。这个坑我在真实项目里踩过目前最朴素的方案是在数据库加一个(contract_id, bill_period)的唯一索引就算代码重复执行数据库层也会拒绝重复插入。缴费核销逻辑相对简单但要注意“部分核销”状态Transactional public void payBill(Long billId, BigDecimal amount, String payChannel) { RentBill bill billMapper.selectByIdForUpdate(billId); BigDecimal newPaid bill.getPaidAmount().add(amount); Assert.isTrue(newPaid.compareTo(bill.getAmount()) 0, 支付金额超过账单总额); bill.setPaidAmount(newPaid); if (newPaid.compareTo(bill.getAmount()) 0) { bill.setStatus(BillStatus.PAID.getCode()); bill.setPayTime(new Date()); } else { bill.setStatus(BillStatus.PARTIAL.getCode()); } billMapper.updateById(bill); // 如果有支付流水表写入支付记录 paymentRecordMapper.insert(new PaymentRecord(billId, amount, payChannel, new Date())); }缴费这块财务的要求就一条每一分钱都能追溯到账单。所以后续系统做在线支付对接时微信支付、支付宝的回调参数里一定要关联billNo回调成功后再做核销更新不能在前台直接改状态。4.5 退租与押金结算退租功能是房产租赁系统里最容易算错账的地方。押金结算要扣水、电欠费如果房屋有损坏还要扣除维修费。设计上我建议走独立一张CheckoutOrder表而不是直接在合同上做标记管家发起退租申请记录退租日期、退租原因。系统联动当前租约状态ACTIVE - TERMINATING。财务抄完水电读数后生成结算单应退押金、扣款明细、实退金额。确认退款后合同状态变成TERMINATED房间恢复EMPTY。这样设计的好处是退租过程是可编辑的、有中间态的不会出现房间已经标记空置但押金退还流程还在半空悬着的情况。财务审计时也能看到完整的结算单。5. 常见问题与排查技巧实录5.1 环境搭建期的典型坑这套系统拿到手第一步是本地跑起来。大多数人在环境搭建阶段卡的并不是代码而是版本兼容性。我在实操项目中遇到最多的几类问题现象根本原因解决方案启动报ClassNotFoundException: javax.servlet.*SpringBoot 3.x 旧版依赖改用SpringBoot 2.7.x或全部升级到jakarta命名空间连接MySQL报Public Key Retrieval is not allowedMySQL 8.0默认caching_sha2_passwordJDBC URL加allowPublicKeyRetrievaltrueuseSSLfalse中文乱码库表字符集不是utf8mb4建库时指定CHARACTER SET utf8mb4时间相差8小时JDBC URL没配时区URL加serverTimezoneAsia/ShanghaiserverTimezone这个坑特别隐蔽本地电脑和服务器时区不同存进去的时间是UTC查出来是东八区所有合同日期全部偏一天。所以从项目初始化第一天起连接串就应该统一加上serverTimezoneAsia/Shanghai。5.2 MyBatis的经典翻车点MyBatis这套框架用起来顺但“顺”的前提是别踩下面几个坑resultType 和 resultMap 混用。当查询结果的字段名和实体类属性名不一致时最稳的方式是给SQL列起别名或用resultMap显式映射。如果只是数据库user_name对应Java里userName下划线自动映射可以先试一下mapUnderscoreToCamelCasetrue配好后能省掉大量别名。动态SQL里if判断失效。经常有人写if teststatus ! null and status ! 但传进来的是Integer或者String。如果是数字类型判断! 永远为真结果就是在不需要过滤状态时也拼接了条件。所以动态SQL的判断条件一定要和Java入参类型匹配。二级缓存开启后数据脏读。MyBatis默认一级缓存是SQL Session级别的二级缓存跨SqlSession。如果你在事务里做了增删改又没有手动清理相关缓存就会出现“插入后查询还是旧数据”的问题。租赁系统这种写操作频繁的业务我建议一开始就不要开启二级缓存等真遇到性能瓶颈再按需开启。5.3 前端联调与部署问题Vue前端和SpringBoot后端联调最常见的是跨域问题。解决方式开发环境用Vite的proxy代理设置如下// vite.config.js export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }生产环境部署如果把前端dist目录丢进SpringBoot的src/main/resources/static记得还要配置一下history路由模式下的fallback否则直接访问/user/dashboard这种前端路由会404Configuration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}/**).forwardTo(/index.html); } }也可以用Nginx单独托管前端静态文件、反代后端API这是更标准的部署方式——前后端分离跑在不同进程里以后前端发版不用重启后端。5.4 事务失效的隐蔽场景Transactional没生效这种事几乎每个新手都会经历一次。最容易踩的情况有同类内部调用this.signContract()从同一个类的方法调用事务注解被“跳过去”。因为Spring事务是通过AOP代理实现的内部调用走的是this没有经过代理对象。解决办法是拆到不同的Service类里注入调用。方法被private修饰私有方法没办法被代理事务注解不生效。异常被吞掉代码里try-catch后正常return事务无法感知异常自然不会回滚。记得在catch块里TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()或者直接往外抛RuntimeException。数据库引擎不支持事务如果表建成了MyISAM引擎事务再怎么写都不会回滚。确认表统一使用InnoDB。边界这块最好在团队里定个规矩所有涉及多表写操作的方法命名就叫xxxTransaction或统一后缀强制所有成员检查这些方法是否满足事务生效条件。不要信“我上个项目没问题”事务失效是整个业务系统最隐蔽的数据杀手。5.5 权限与状态不一致还有一个问题我放在最后说系统的状态是靠数据库字段撑着的但状态改了不代表业务流程跑对了。我建议在这个项目里做一个每天凌晨执行的“数据一致性校验任务”逻辑就是扫描所有ACTIVE合同对应的房间如果房间状态不是RENTED输出异常清单。扫描所有UNPAID且已超过due_date的账单如果没有被标记为OVERDUE自动修复。扫描所有LOCKED超过30天的房间提醒管家确认是否要取消锁定。这类自动对账任务不需要多复杂但对保证一个业务系统长期稳定运行非常有效。很多人把系统上线当成了终点其实上线之后的状态自愈能力才是企业级系统的分水岭。写到最后聊几句实在的优化建议这套SpringBoot Vue MyBatis MySQL的租赁系统框架跑通并上线之后如果想让它真正达到“企业级”的水准我个人建议按三个优先级逐步升级第一优先把支付通道从线下核销升级为微信/支付宝在线支付加上支付回调、退款接口、对账单逻辑后端多做一层防重、防篡改校验。第二优先把租客端的Web页面改成微信小程序或者H5让租客能自助查账单、交费、报修前台管家的工作量立刻降一半。第三优先引入Redis做缓存和分布式会话目前用的数据库直查在数据量到万级后会有明显压力缓存层会轻松很多。踩过几次坑之后我最大的体会是技术栈从来不是项目成败的关键关键是对业务状态流转的理解、对事务边界的敬畏、以及对数据一致性的较真。这套源码真正值钱的地方就是它把这些经验都落到了代码里让你不用从头把坑都踩一遍。
返回列表