Spring Boot房屋租赁系统毕设:表结构、核心代码与答辩全攻略

发布时间:2026/10/30 15:03:12
Spring Boot房屋租赁系统毕设:表结构、核心代码与答辩全攻略

简介:面向Java初学与毕业设计人群的房屋租赁管理系统完整项目包,基于SpringBoot框架开发,覆盖管理员、房东、用户三类角色,包含房源发布、信息审批、订单管理等核心业务,可用于课程设计、毕业设计或JavaWeb开发练习。资源共913个文件,以Java源码、Vue前端、JS脚本、CSS样式、SQL数据库脚本为主,同时提供论文文档与答辩PPT,压缩包大小仅24.22MB,结构清晰便于按模块学习。目前已有63人学习下载,适合需要完整可运行项目参考的开发者。资源内含安装运行批处理脚本、数据库初始化文件以及源码工程配置,论文目录涵盖绪论、开发环境、系统分析、数据库设计、功能实现与系统测试等完整章节,可快速理解房屋租赁系统从需求分析到上线测试的整个过程,帮助读者掌握SpringBoot与JSP技术整合开发,并为毕业设计答辩提供直接素材。

1. 一个能跑、能写论文、能答辩的 Spring Boot 房屋租赁系统,值不值得做

“基于 Spring Boot 的房屋租赁系统”在 Java 毕设里的热度一直不低,但“源码+论文+ppt答辩”这个标题的完整含义不是“写个网页”,而是同时交付三件套:一套能演示的 Spring Boot 工程、一篇和代码对得上号的毕业论文、一份答辩时能讲清逻辑的 PPT。对正在选毕设题目的学生来说,这个方向的最大优点是业务边界清楚:房屋、租客、合同、账单、到期提醒,每个模块都能独立演示,也能串成一条租赁业务闭环。对想快速上手 Spring Boot 的开发者,它又是一套标准的控制器-服务-数据访问分层练习。这篇笔记不打算复刻某份现成源码,而是按最常见可靠的实现路径,把表结构、核心代码、论文组织和演示避坑一次讲透。

2. 先立骨架再写代码:Spring Boot 房屋租赁系统的表结构与接口规划

2.1 技术选型:为什么这套组合对毕设体量刚刚好

Spring Boot 是当前 Java Web 毕设里最主流的框架选项,原因是自动配置、起步依赖和内嵌 Tomcat 把项目从“要写一堆配置才能启动”变成“加了依赖就能跑”。房屋租赁系统按功能模块拆,有房屋管理、租客管理、合同管理、账单管理、统计看板五块,CRUD 占大多数,真正的业务规则集中在签合同、算租金、查到期这三处。因此最常见的组合是 Spring Boot + MyBatis-Plus + MySQL 三件套,前端用 Thymeleaf 服务端渲染,或者用 Vue 做前后端分离。

这里要提前做决定的是前端方向。Thymeleaf 能把控制器方法直接映射到页面,省去跨域、Token 传输这些额外讲解,写论文时“控制层-页面”的对应关系非常直白;Vue 则要多写接口联调和前端路由,工作量更大,但更贴近企业项目。毕设阶段如果时间紧,我一般建议用 Thymeleaf 起步,把预算留给库存判断这类业务逻辑。更重的 Spring Cloud 和老旧的 SSH 框架都不是这个题目的最优解:前者要解释“为什么小系统要做服务治理”,后者在“框架应用”这一栏显得单薄且找不到新意。

项目结构按标准三层来走:controller 接收 HTTP 请求,service 放业务规则,mapper 管数据访问。实体类放在 entity,请求和响应对象单独放 dto、vo。包名按 com.example.house 这种方式组织,不要把所有类塞进一个包,否则后面论文里的功能模块图会很难画。这个结构在答辩时基本不会被挑战,因为它是 Spring Boot 社区最通用的分层方式。

2.2 核心表结构:房、客、合同、账单四张表的建表 SQL

建表是动手第一件事,也是最容易返工的一步。房屋租赁系统的核心不是“房子”而是“合同”,因为租金、账单、到期提醒全部从合同派生。所以表结构至少要覆盖四张核心表:房屋表记录房源基础信息和当前状态,租客表记录承租方信息,合同表把房屋、租客、起止时间和成交租金关联起来,账单表按合同按月生成应缴和实缴记录。

-- 房屋表:核心字段与状态 CREATE TABLE house ( id BIGINT AUTO_INCREMENT PRIMARY KEY, community VARCHAR(50) NOT NULL COMMENT '小区名称', building_no VARCHAR(10) COMMENT '楼栋号', room_no VARCHAR(20) NOT NULL COMMENT '房号', area DECIMAL(6,2) COMMENT '建筑面积(㎡)', layout VARCHAR(20) COMMENT '户型,如两室一厅', monthly_rent DECIMAL(10,2) NOT NULL COMMENT '月租金(元)', status TINYINT DEFAULT 0 COMMENT '0未出租 1已出租 2维修中', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_house_unit (community, building_no, room_no) ); -- 租客表:字段按登记和签约需要设计 CREATE TABLE tenant ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL COMMENT '姓名', phone VARCHAR(20) NOT NULL COMMENT '手机号', id_card VARCHAR(18) COMMENT '身份证号,演示系统建议脱敏', emergency_contact VARCHAR(30) COMMENT '紧急联系人', emergency_phone VARCHAR(20) COMMENT '紧急联系电话', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 合同表:状态与租金快照是重点 CREATE TABLE lease_contract ( id BIGINT AUTO_INCREMENT PRIMARY KEY, contract_no VARCHAR(32) NOT NULL COMMENT '合同编号', house_id BIGINT NOT NULL COMMENT '房屋ID', tenant_id BIGINT NOT NULL COMMENT '租客ID', start_date DATE NOT NULL COMMENT '起租日期', end_date DATE NOT NULL COMMENT '退租日期', monthly_rent DECIMAL(10,2) NOT NULL COMMENT '成交月租,租金快照', deposit DECIMAL(10,2) DEFAULT 0 COMMENT '押金金额', status TINYINT DEFAULT 1 COMMENT '1生效中 2已退租 3已到期', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_contract_no (contract_no), KEY idx_house_dates (house_id, start_date, end_date) ); -- 账单表:唯一键防止同一合同同月重复生成 CREATE TABLE bill ( id BIGINT AUTO_INCREMENT PRIMARY KEY, contract_id BIGINT NOT NULL COMMENT '合同ID', bill_month CHAR(7) NOT NULL COMMENT '账单月份,如2026-03', amount DECIMAL(10,2) NOT NULL COMMENT '应收金额', water_fee DECIMAL(8,2) DEFAULT 0 COMMENT '水费', power_fee DECIMAL(8,2) DEFAULT 0 COMMENT '电费', status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付', paid_time DATETIME COMMENT '实付时间', UNIQUE KEY uk_contract_month (contract_id, bill_month) );

这段 SQL 里有三个设计值得在论文里展开。房屋表的唯一键 uk_house_unit 把“同一小区同一楼栋同一房号不能重复录入”交给数据库兜底,比在 service 里先 selectCount 再判空更可靠;合同表的 monthly_rent 是租金快照,合同签订那一刻把房屋表当前的月租拷贝过来,之后房屋调价不影响已签合同的对账;账单表的 uk_contract_month 唯一键保证定时任务重复跑、按钮被连点两次都不会生成两份同月账单。

金额字段一律用 DECIMAL 而不是 DOUBLE,这是新手项目里最常见的坑之一。DOUBLE 在浮点计算时会出现 0.1+0.2 不等于 0.3 的精度问题,租金、押金一旦出现误差,演示时填 3400 元房租实际存成 3399.9999 就很尴尬。日期字段用 DATE 记录起止日期,DATETIME 记录创建和实付时间。另外补一张 admin 表做后台登录,字段只有 id、username、password_hash,密码用 BCrypt 哈希后入库;用拦截器校验登录态即可,不必上 Spring Security,否则配置比业务还多。

2.3 接口规划:先列 API 再写代码,论文的模块图直接照这个画

建完表之后、动手写 Controller 之前,我建议先花半小时把 API 清单列出来。这个习惯的好处是论文第 4 章“功能模块设计”的图就是这张清单的图形化,答辩时评委问“系统有哪些功能”,也可以直接对着接口回答。房屋租赁系统按模块拆,最少需要这些接口:

功能模块接口方法说明
房屋管理/api/house/pageGET分页查询,支持状态和关键字过滤
房屋管理/api/housePOST / PUT新增/编辑房屋
房屋管理/api/house/{id}DELETE仅限未出租房屋逻辑删除
租客管理/api/tenant/pageGET租客分页列表
租客管理/api/tenantPOST / PUT新增/编辑租客
合同管理/api/contractPOST签订合同,带时间重叠校验
合同管理/api/contract/{id}/terminatePUT退租,释放房屋
账单管理/api/bill/generatePOST手动触发当月账单生成
账单管理/api/bill/pageGET账单分页查询
看板统计/api/dashboard/summaryGET出租率、应收/实收汇总

接口返回统一封一层 Result<T>,包含 code、message、data 三个字段。代码里正常路径写 return Result.ok(...),业务异常直接 throw new BizException("该时间段已有生效合同"),再配一个全局异常处理器统一定制返回。这样的好处是前端不用针对每个接口做异常分支,答辩时“统一返回结构 + 全局异常处理”是一个能单独拎出来讲的亮点。

3. 从查询到合同生成:租赁核心业务的实现要点与关键代码

3.1 房屋分页查询:MyBatis-Plus 分页插件与条件查询

房屋列表是系统打开后的第一个页面,功能很基础,但“分页 + 关键字 + 状态过滤”组合在一起,就足够演示 Spring Boot 里最常用的一套查询写法。用 MyBatis-Plus 的 Page 对象和 LambdaQueryWrapper 实现:

@GetMapping("/api/house/page") public Result<IPage<HouseVO>> page( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Integer status, @RequestParam(required = false) String keyword) { // 1. 创建分页对象,页码从1开始 Page<House> page = new Page<>(pageNum, pageSize); // 2. 构建条件:状态精确过滤,关键字模糊匹配 LambdaQueryWrapper<House> wrapper = new LambdaQueryWrapper<>(); if (status != null) { wrapper.eq(House::getStatus, status); } if (StringUtils.hasText(keyword)) { wrapper.and(w -> w.like(House::getCommunity, keyword) .or().like(House::getRoomNo, keyword)); } wrapper.orderByAsc(House::getId); // 3. 执行分页查询,Page对象会接收total等分页数据 IPage<House> housePage = houseMapper.selectPage(page, wrapper); // 4. 转成VO后返回,避免直接暴露实体字段 return Result.ok(housePage.convert(house -> houseService.toVO(house))); }

参数上有几个细节。pageNum 默认 1,pageSize 默认 10,前端不传也能跑通,这是接口健壮性的基本要求。keyword 用 StringUtils.hasText 而不是字符串 equals 判断,能同时排除 null 和空字符串。LambdaQueryWrapper 通过方法引用来指定字段,字段名在编译期就能被发现写错,这也是论文里“为什么选 MyBatis-Plus”时的加分回答。

关键点在第 3 步:MyBatis-Plus 的分页不是自动生效的,必须在配置类里注册分页插件,否则 selectPage 会退化成查全表再内存分页。配置代码如下:

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 指定MySQL方言,分页SQL才会带上LIMIT interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

注册之后观察控制台打印的 SQL,能看到 LIMIT 关键字,这才叫分页真正生效。这个配置要放在一个带 @Configuration 的类里,而不是写在 Controller 内部,否则扫描不到。

3.2 签订合同:时间重叠校验与事务边界

合同模块是整个系统的业务核心,答辩时评委大概率会问“同一间房会不会被重复租出去”。这个问题在代码上分三层防护:第一层校验房屋状态,只有“未出租”才能签新合同;第二层做时间区间重叠校验,同一房屋不能有两份生效合同的时间段相交;第三层靠数据库唯一索引兜底。签合同的核心方法:

@Transactional(rollbackFor = Exception.class) public Long createContract(ContractCreateDTO dto) { // 1. 房屋状态校验:只有未出租才能签 House house = houseMapper.selectById(dto.getHouseId()); if (house == null || house.getStatus() != 0) { throw new BizException("该房屋当前不可出租"); } // 2. 时间重叠校验:新合同起点早于已有合同终点,且新合同终点晚于已有合同起点 LambdaQueryWrapper<Contract> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Contract::getHouseId, dto.getHouseId()); wrapper.eq(Contract::getStatus, 1); wrapper.le(Contract::getStartDate, dto.getEndDate()); wrapper.ge(Contract::getEndDate, dto.getStartDate()); Long count = contractMapper.selectCount(wrapper); if (count != null && count > 0) { throw new BizException("该时间段已有生效合同,请勿重复出租"); } // 3. 租金快照:成交租金以签订时房屋租金为准 Contract contract = new Contract(); contract.setContractNo(generateContractNo(house.getId())); contract.setHouseId(house.getId()); contract.setTenantId(dto.getTenantId()); contract.setStartDate(dto.getStartDate()); contract.setEndDate(dto.getEndDate()); contract.setMonthlyRent(house.getMonthlyRent()); contract.setStatus(1); contractMapper.insert(contract); // 4. 房屋状态置为已出租 houseMapper.updateById(House.builder().id(house.getId()).status(1).build()); return contract.getId(); }

时间重叠那段 SQL 是整个方法里最需要理解的地方:新合同的开始日期小于等于已有合同的结束日期,且新合同的结束日期大于等于已有合同的开始日期,只要查出来有记录,就说明时间区间相交。这个条件初看别扭,画一条时间轴就清楚了。A 合同从 2026-03-01 到 2026-08-31,B 合同想签 2026-07-01 到 2026-12-31,B 的 startDate 2026-07-01 小于 A 的 endDate 2026-08-31,同时 B 的 endDate 2026-12-31 大于 A 的 startDate 2026-03-01,于是被拦截。如果条件写错,边界日期就会出现重复出租,比如旧合同结束当天被新合同占用。

事务注解 @Transactional(rollbackFor = Exception.class) 把“插合同”和“改房屋状态”绑成一个原子操作,中间任何一步抛异常,前面已经执行的 insert 或 update 全部回滚。这里特别要注意 rollbackFor:Spring 默认只在遇到 RuntimeException 时回滚,如果方法里抛的是自定义受检异常,不加 rollbackFor 就会事务不生效,合同插进去了但房屋状态没有改成已出租。这个坑属于“看日志看不出问题,看数据才知道坏了”的典型现场。

3.3 按月账单与到期提醒:定时任务怎么写不翻车

房屋租赁系统做完 CRUD 之后,让评委觉得“有业务深度”的是自动化和提醒。账单按月生成、合同到期提醒都可以用 Spring Boot 自带的 @Scheduled 注解实现,不需要引入 Quartz。一个典型的账单生成任务:

// 每月1号凌晨2点执行:分0秒0 时2 日1 @Scheduled(cron = "0 0 2 1 * ?") public void generateMonthlyBill() { LocalDate today = LocalDate.now(); // 只处理还未到期、且状态为生效中的合同 List<Contract> activeContracts = contractMapper.selectList( new LambdaQueryWrapper<Contract>() .eq(Contract::getStatus, 1) .ge(Contract::getEndDate, today)); for (Contract contract : activeContracts) { Bill bill = new Bill(); bill.setContractId(contract.getId()); bill.setBillMonth(YearMonth.now().toString()); bill.setAmount(contract.getMonthlyRent()); bill.setStatus(0); // 待支付 try { billMapper.insert(bill); } catch (DuplicateKeyException e) { // 唯一键兜底:重复执行任务时跳过已生成的账单 log.warn("账单已存在,跳过 contractId={} month={}", contract.getId(), bill.getBillMonth()); } } }

cron 表达式 “0 0 2 1 * ?” 拆开是:秒 0、分 0、时 2、日 1、月 *、星期 ?,也就是每个月 1 号凌晨 2 点执行。选凌晨 2 点而不是 1 点,是因为很多 MySQL 实例把定时备份也放在凌晨,避开高峰期可以减少锁表冲突,这个细节写进论文“非功能需求”一节会显得考虑过生产环境。任务里先查“生效中且结束日期大于等于今天”的合同,避免给已经退租的合同继续生成账单。

账单为什么会重复生成?定时任务跑第二次、人为手动触发、接口被重复请求,都可能让同一个月同一份合同出现两条账单。前面表结构里的 uk_contract_month 唯一键在这里兜底:第二次 insert 抛 DuplicateKeyException,代码捕住异常打一条 warn 日志跳过。如果不在代码里捕异常,定时任务会因为这条重复记录中断,后面合同的账单全部生成不了。

到期提醒的思路更简单,每天上午 8 点扫一次合同,把 30 天内到期的生效中合同筛出来,逐个生成未读提醒:

@Scheduled(cron = "0 0 8 * * ?") public void scanExpiringContracts() { LocalDate deadline = LocalDate.now().plusDays(30); List<Contract> expiring = contractMapper.selectList( new LambdaQueryWrapper<Contract>() .eq(Contract::getStatus, 1) .le(Contract::getEndDate, deadline)); expiring.forEach(c -> reminderService.add( c.getContractNo(), "合同将于" + c.getEndDate() + "到期")); }

数据量上去以后,扫描全表会变慢,可以给 lease_contract 的 (status, end_date) 加联合索引,让查询走索引而不是全表扫描。毕设阶段在论文“数据库优化”一节提这个联合索引,是很扎实的一笔。

4. 论文和 PPT 答辩怎么跟代码对上:把三件套做成一件事

4.1 论文章节安排:每个章节分别对应源码里的哪一部分

很多项目代码写完,论文才发现无从下笔,本质原因是代码和文档没有在同一节奏上推进。其实毕业论文的结构高度固定,房屋租赁系统这种业务型题目尤其好写,按照下面这张表来填内容,章节和源码的对应关系就很清楚:

论文章节写什么对应源码/材料
摘要一句话概括系统目标、技术栈、功能模块和结论写完后对照代码核对每个功能点
绪论背景与意义、国内外现状、研究内容房屋租赁市场规模、中介管理痛点、为什么选 Spring Boot
需求分析功能性需求、非功能性需求、用例图接口清单拆成用例
系统设计总体架构、功能模块图、数据库 ER 图、核心表设计建表 SQL 和模块划分
系统实现每个功能模块一个页面 + 核心代码片段房屋管理页、合同签订流程、账单定时任务
系统测试测试环境、测试用例表、测试结果JUnit 用例 + 手工演示记录
总结与展望做了什么、达到什么效果、还能怎么改权限细化、报表导出、消息推送

论文第 4 章“系统实现”的每个小节,都配一张“运行截图 + 一段核心代码 + 两段解释”。截图像素要清晰,代码不长于 15 行。截图右上角的浏览器标签页里最好露出系统标题,避免被看出是网上下载的套图。代码从自己项目里复制,答辩时老师问“这段代码在哪个类里”,能打开 IDEA 现场指出来才算数。

需求分析章节最容易写成“百度百科搬运”。更可靠的写法是把自己的接口清单翻译成用例:把 POST /api/contract 展开成“租客选中房源、选择起止日期、系统校验时间冲突、生成合同、置房屋为已出租”这样带前置条件和后置条件的用例描述。这样写出来既是论文内容,也是答辩时讲解脚本的一部分。

4.2 PPT 的 12 页结构:演示 Demo 配哪几页讲最顺

答辩 PPT 控制在 12 页左右,每页一个主题,只放结论不放大段文字。房屋租赁系统的页面分布可以按下面这张表来组织,核心思想是“演示和讲稿穿插”,不要一口气讲完 10 页 PPT 再开 Demo。

页码页面内容讲法/配合
1封面题目、姓名、学号、导师
2研究背景与目标房屋租赁管理痛点,30 秒讲完
3技术选型放项目结构截图,讲 Spring Boot 和 MyBatis-Plus 的选型理由
4系统功能架构五个功能模块图,讲模块间关系
5数据库设计ER 图,讲租金快照和账单唯一键
6核心业务合同签订流程,讲时间重叠校验
7系统演示(一)房屋管理、分页查询、关键字过滤
8系统演示(二)合同签订、账单生成、到期提醒
9核心代码时间重叠校验 + 事务注解,对应第 6 页
10系统测试测试用例表
11总结与展望做完了什么,还能做什么
12致谢/Q&A快速收尾,准备回答问题

演示时自己手输 URL 容易紧张,建议提前把核心页面加到浏览器书签。数据要“像真的”:十套房源分布在 3 个小区,租金在 800 到 6000 之间拉开梯度,租客姓名用常见的真人感名字,别用 test01、test02。演示房屋管理时切到第 4 页功能架构,演示合同签订时切回第 5 页 ER 图,观众看着界面上的状态变化,再对照架构图听你讲,信息才接得住。

4.3 答辩高频问题:为什么、怎么办、哪里体现

答辩本质是验证“代码是不是你写的、系统有没有想清楚”。围绕 Spring Boot 房屋租赁系统,被问频率最高的几个问题,回答方向整理如下:

高频问题回答要点
为什么选 Spring Boot?自动配置 + 起步依赖减少样板配置,内嵌 Tomcat 一条命令启动,结合项目里用到的 starter
为什么用 MyBatis-Plus?单表 CRUD 不用手写 SQL,分页插件统一处理分页,LambdaQueryWrapper 防止字段名写错
同一间房会不会重复出租?三道防线:房屋状态、时间重叠查询、唯一键兜底;时间重叠条件画时间轴解释
到期提醒怎么实现?@Scheduled 定时扫描,加状态和截止日期联合索引,演示定时任务日志
金额为什么用 DECIMAL?DOUBLE 有浮点精度误差,金额场景不接受 3399.9999
为什么把成交租金存到合同表?租金快照,房屋调价不影响历史合同和账单对账
事务加在哪里?签订合同方法上,@Transactional(rollbackFor = Exception.class),插合同和改状态必须同生共死
用户量变大了怎么办?先答优化点:Redis 缓存热门房源、账单生成改队列异步、读写分离,不要硬说自己做过
测试是怎么做的?讲测试用例表:正常租房、时间重叠、非法房态、重复生成账单四个场景
密码安全有没有考虑?登录密码 BCrypt 加密,不存明文,管理员表单独做权限校验

答不清的时候就用代码说话。比如时间重叠问题,直接翻到 Service 类里那几行 selectCount,比口述十句话都有说服力。

5. 常见问题排查:Spring Boot 房屋租赁系统的几个经典翻车现场

5.1 Spring Boot 版本太高,JDK 8 环境直接启动失败

现象:本地 JDK 8 启动 Spring Boot 项目,Maven 构建阶段就报错,或者启动过程中抛出 java.lang.UnsupportedClassVersionError,以及大量 javax.servlet 相关类找不到的异常。

原因:Spring Boot 3.x 系列的最低要求是 JDK 17,包名也从 javax 迁移到了 jakarta。很多毕设电脑默认装的是 JDK 8,网上部分教程和源码包还在用 2.x,两边一撞,启动日志里全是类找不到。

解决:把项目固定在 Spring Boot 2.7 系列,父 POM 不要引 3.x 的版本;确认 IDE 的 Project SDK 选的是本地 JDK 8。2.7 系列对自动配置、starter 机制的支持很完整,写论文完全够用。快速检查方式:看项目里 import 的是 javax.servlet 还是 jakarta.servlet,后者说明版本进入 3.x 时代了。

5.2 分页接口查出全部数据,Page 参数形同虚设

现象:调用 /api/house/page 传 pageNum=2&pageSize=5,返回结果却依然是前 10 条或全部数据,total 字段也不对。

原因:MyBatis-Plus 的分页插件没有注册。selectPage 依赖配置类里的 PaginationInnerInterceptor 来改写 SQL,没有插件时只能做“查全表 + 内存分页”的效果,数据库层面没有 LIMIT,数据一多就崩。

解决:在配置类里注册 MybatisPlusInterceptor,并显式传入 DbType.MYSQL。注册后观察控制台打印的 SQL,能看到 LIMIT 关键字,这是分页真正生效的标志。另一个排查点是确认 Controller 方法返回的是 IPage 而不是 List,也别在调用 selectList 之后手动 new Page 却不传参。

5.3 合同起止日期和账单月份错位,退租当月应收算不清

现象:合同从 2026-03-15 到 2027-03-14,系统每月 1 号按整月生成 3400 元账单。退租结算时,2027-03 只住了 14 天,账单却是整月 3400 元,于是出现“多收退租当月、少算首月实际天数”的对不上账。

原因:账单按自然月生成,而合同按签约周期起租,两者天然不对齐。如果首尾月不做按天折算,就会出现多收少退。

解决:把业务规则在论文里写清楚:首月按实际入住天数折算,尾月按实际住满天数结算,中间月份按整月生成。账单生成代码里让 contract 的 start_date 参与计算,首月金额 = 月租金 / 当月总天数 × 实际入住天数,而不是直接 setAmount(月租金)。退租模块也要配套一个“按本月已住天数生成退租账单”的方法,否则演示到退租时会当场对不上数。

5.4 接口直接返回实体类,JSON 序列化循环引用导致数据丢失

现象:合同列表接口返回后,前端渲染的房屋信息和租客信息要么是 null,要么控制台直接报序列化错误,日志里出现 infinite recursion 或 Could not write JSON。

原因:实体类上加关联字段,比如 Contract 实体里放了一个 House 对象,House 对象里又引用 List<Contract>,Jackson 序列化时陷入循环。

解决:所有返回给前端的数据走 VO,Controller 的返回类型不要写实体类。ContractVO 里放 houseId、community、roomNo、tenantName 这类扁平字段,在 Service 层组装数据。这个做法同时解决了“实体字段直接暴露给前端不安全”的问题,答辩时提到 VO/DTO 分离又是一处加分点。

6. 把系统做厚:从“能跑”到“能答辩、能演示”的验证技巧

写代码到一半的时候,我习惯先把“演示剧本”列出来:系统打开后先给评委看哪个页面、点哪个按钮、预期出现什么数据。没有剧本的演示最容易在答辩现场冷场——点开房屋管理发现只有两条测试数据,或者定时任务还没到执行时间,评委看不出系统在跑什么。

一个实用的做法是准备一套“最小但像真”的演示数据:3 个小区共 10 套房,4 个租客,2 份生效合同加 1 份历史到期合同,账单覆盖最近 3 个月。数据量少不代表失真,关键在数据之间有逻辑关系:某套房状态是“已出租”,对应的合同恰好处于生效期;某间房在 5 天前刚到期,提醒列表里就躺着一条到期告警。演示时先讲数据,再讲操作,界面上的每个状态都对应得上前面的表设计。

验证工作不要只靠浏览器手点。我一般会在项目里补三个最小化的单元测试:合同时间重叠校验(构造两份时间交叉的合同断言抛异常)、账单重复生成(同一个 contract_id 和 bill_month 插两次断言第二次失败)、到期提醒查询(把合同结束日期设为 10 天后断言列表长度)。这三个测试能在答辩前自动筛掉最可能翻车的三个点,而且测试代码放进论文“系统测试”章节非常加分。测试类加 @SpringBootTest 依赖 spring-boot-starter-test,跑 mvn test 即可。

演示前还有两个小习惯。第一,把 Spring Boot 的日志级别调到 DEBUG,或者打开 MyBatis 的 SQL 日志,演示时让评委看到控制台打印的 SELECT 语句,比任何架构图都有说服力。第二,用 Apifox 或 Postman 把核心接口存成一个请求集合,CRUD、合同签订、账单生成、到期扫描各留一条,现场如果页面操作失灵,切到接口工具里一条一条讲逻辑,也能把场面救回来。

我做这类项目最大的教训,是别把代码全部写完再开始写论文。论文的模块图和数据库 ER 图,在表结构和接口清单定稿的那天就可以画出来;后面每完成一个功能模块,就往论文“系统实现”章节里填一段代码和截图。等系统跑通时论文初稿已经成型,答辩 PPT 只是把论文里的图抽出来排一遍版,而不是从零开始赶工。希望这个工作顺序和上面的避坑清单能帮到你,让你的 Spring Boot 房屋租赁系统真的做到“能跑、能写、能讲”三件事一起交付。

本文还有配套的精品资源,点击获取