Java房屋租赁系统课程设计:Spring Boot全栈实战与状态机设计
简介本资源为基于Java的房屋租赁系统毕业设计文档面向计算机相关专业学生及需要完成课程设计或论文写作的开发者。文档围绕房屋租赁系统的完整开发流程展开涵盖研究背景、需求分析、可行性分析、系统流程梳理、架构设计、数据库实体与表设计、系统实现及测试等环节并涉及Java、MySQL、B/S结构、JSP等关键技术选型说明。资源包共1个docx文件大小约3.86MB内容为完整的论文式文档目录结构清晰包含摘要、前言、开发环境、需求分析、架构设计、系统实现、系统测试、结论、参考文献与致谢等章节便于读者直接参考写作框架与设计思路。目前已有51人学习下载适合需要快速搭建论文结构、理解系统设计流程或作为项目开发参考的读者使用。1. 基于 Java 的房屋租赁系统从课程设计到能跑通的最小闭环做过 Java 课程设计的人大概都有这种体验题目发下来一看是「房屋租赁系统」心里先松一口气——不就是增删改查吗真动手才发现房源状态怎么流转、租客和房东怎么区分权限、合同到期怎么自动提醒这些才是真正卡人的地方。这个标题背后其实是一套完整的业务系统设计房东发布房源、租客浏览筛选、双方签约生成合同、租期到期释放房源中间还夹着押金、账单、维修工单这些边角料。它适合两类人一类是正在做 Java 课程设计、需要一套能讲清楚设计思路的参考方案的学生另一类是想拿它练手 Spring Boot 全栈、顺便把面向对象编程 Java 的建模能力补一补的初学者。下面我按自己实际搭过一遍的顺序把选型、建表、核心接口和踩过的坑讲清楚你照着能跑出一个最小可用版本再往上加功能就有底了。2. 技术选型与领域建模为什么不用纯 Servlet 硬写2.1 分层架构的取舍Spring Boot MyBatis 还是 JSP JDBC课程设计里最常见的两种写法一种是纯 JSP Servlet JDBC另一种是 Spring Boot MyBatis。前者不是不能做但房屋租赁系统的实体关系偏多——用户、房源、合同、账单、维修单五张表起步纯 JDBC 意味着每个 DAO 都要手写连接获取、PreparedStatement 赋值、ResultSet 映射一个字段改名要改三处。我一般会直接上 Spring Boot理由很实在依赖注入把 Service 和 Mapper 解耦事务用Transactional一行注解搞定省下来的时间可以花在业务逻辑上。MyBatis 相比 JPA 更适合这个场景因为租赁系统里有大量条件组合查询——按区域、价格区间、户型、朝向、是否带电梯筛选房源动态 SQL 用if标签拼比 JPA 的 Specification 直观得多。如果你对 JPA 更熟用 Spring Data JPA 也能做只是多条件筛选那块要写不少 Criteria 代码。数据库选 MySQL 8字符集用utf8mb4因为房源描述里可能有 emoji 或者生僻字。连接池用 HikariCPSpring Boot 默认就带不用额外配。2.2 核心实体与表结构五张表撑起最小闭环先把领域模型定下来后面写代码才不会反复改表。最小闭环需要这几张表表名作用关键字段user用户房东/租客/管理员id, username, password, role, phonehouse房源id, landlord_id, title, address, price, status, area, room_typecontract租赁合同id, house_id, tenant_id, start_date, end_date, deposit, statusbill账单id, contract_id, amount, due_date, pay_statusmaintenance维修工单id, house_id, tenant_id, description, statushouse.status用枚举值控制流转0待租、1已租、2下架。这个字段是整个系统的状态机核心签约时置 1合同到期或退租时置 0房东主动下架置 2。很多同学在这里翻车是因为把状态判断散落在各个 Service 里改一处漏一处。我的做法是抽一个HouseStatusService所有状态变更都走它内部校验「当前状态能否转到目标状态」非法流转直接抛业务异常。contract表要存start_date和end_date这两个字段决定了账单生成和到期提醒。押金deposit单独存不要和租金混在一起退租时押金退还是独立流程。2.3 用面向对象把「租赁」这件事抽象出来面向对象编程 Java 的功夫在这里体现。不要一上来就写 Controller先把行为想清楚一个房源能被「发布」「下架」「签约锁定」一份合同能「生成账单」「到期结算」「提前解约」。这些行为对应到类上House实体里放状态字段但状态流转的方法放在 Service 层因为流转往往要联动其他表——签约要同时改房源状态、插合同记录、生成首期账单这三步必须在一个事务里。Service public class ContractService { Autowired private HouseMapper houseMapper; Autowired private ContractMapper contractMapper; Autowired private BillMapper billMapper; Transactional(rollbackFor Exception.class) public Long signContract(Long houseId, Long tenantId, LocalDate start, LocalDate end, BigDecimal deposit) { // 1. 校验房源当前是否可租 House house houseMapper.selectById(houseId); if (house null || house.getStatus() ! 0) { throw new BizException(房源不可租); } // 2. 锁定房源防止并发重复签约 int updated houseMapper.updateStatus(houseId, 0, 1); if (updated 0) { throw new BizException(房源已被他人签约); } // 3. 插入合同 Contract contract new Contract(); contract.setHouseId(houseId); contract.setTenantId(tenantId); contract.setStartDate(start); contract.setEndDate(end); contract.setDeposit(deposit); contract.setStatus(1); contractMapper.insert(contract); // 4. 生成首期账单 Bill bill new Bill(); bill.setContractId(contract.getId()); bill.setAmount(house.getPrice()); bill.setDueDate(start.plusDays(7)); bill.setPayStatus(0); billMapper.insert(bill); return contract.getId(); } }这段代码的关键在第二步updateStatus(houseId, 0, 1)用的是带旧状态条件的更新SQL 类似UPDATE house SET status 1 WHERE id ? AND status 0。返回影响行数为 0 说明有人抢先改了状态直接抛异常回滚。这是防并发重复签约最省事的做法比先查再改可靠得多。参数上rollbackFor Exception.class保证任何异常都回滚别用默认的只回滚运行时异常业务异常如果是受检异常就漏了。3. 从建表到接口把房源发布和筛选跑通3.1 建表 SQL 与索引三个容易忽略的细节建表看着简单但索引没加对后面筛选房源会慢得离谱。下面是我实际用的建表语句核心部分CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT, landlord_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, address VARCHAR(200) NOT NULL, price DECIMAL(10,2) NOT NULL, area DECIMAL(6,2) DEFAULT NULL, room_type VARCHAR(20) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status_price (status, price), KEY idx_landlord (landlord_id), KEY idx_address (address(20)) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三个细节第一idx_status_price是联合索引因为最高频的查询是「查所有待租房源按价格排序」status 在前 price 在后能同时命中过滤和排序。第二address建前缀索引因为地址字段长但区分度集中在前 20 个字符全字段索引浪费空间。第三price用DECIMAL不用FLOAT金额计算浮点数会有精度问题这个坑面试和实战都常考。3.2 房源发布接口参数校验别偷懒发布接口看着就是插一条记录但校验不做全后面全是脏数据。用 Spring Validation 在 DTO 上标注解public class HousePublishDTO { NotBlank(message 标题不能为空) Size(max 100, message 标题过长) private String title; NotBlank(message 地址不能为空) private String address; NotNull(message 价格不能为空) DecimalMin(value 0.01, message 价格必须大于0) private BigDecimal price; DecimalMin(value 1.0, message 面积不合理) private BigDecimal area; private String roomType; // getter/setter 省略 }Controller 上加Valid校验失败会抛MethodArgumentNotValidException用一个RestControllerAdvice统一捕获返回友好提示。这里有个容易漏的点landlord_id不要从请求参数里取要从登录态里拿。否则租客能伪造 landlord_id 替别人发房源。登录态用 Session 或 JWT 都行课程设计里 Session 更简单拦截器里把 userId 塞进 ThreadLocalService 层直接取。3.3 多条件筛选房源动态 SQL 怎么写才不翻车筛选是租赁系统的门面用户会同时传区域、价格区间、户型、排序方式。MyBatis 的if标签处理这种场景很顺手select idselectByCondition resultTypeHouseVO SELECT h.*, u.username AS landlordName FROM house h LEFT JOIN user u ON h.landlord_id u.id where h.status 0 if testaddress ! null and address ! AND h.address LIKE CONCAT(%, #{address}, %) /if if testminPrice ! null AND h.price gt; #{minPrice} /if if testmaxPrice ! null AND h.price lt; #{maxPrice} /if if testroomType ! null and roomType ! AND h.room_type #{roomType} /if /where ORDER BY choose when testsort priceAsch.price ASC/when when testsort priceDesch.price DESC/when otherwiseh.create_time DESC/otherwise /choose LIMIT #{offset}, #{pageSize} /select注意where标签会自动去掉开头多余的 AND不用手写WHERE 11。排序用choose而不是直接拼字符串因为${}拼接有 SQL 注入风险虽然这里 sort 是枚举值可控但养成用choose的习惯更稳。分页参数offset和pageSize在 Service 层算好传进来offset (pageNum - 1) * pageSize。提示LIKE CONCAT(%, #{address}, %)这种前置模糊匹配用不上索引数据量上万后会明显变慢。课程设计阶段数据量小无所谓但如果想优化可以改成全文索引或者引入 Elasticsearch不过那是另一个话题了。4. 合同、账单与到期处理业务逻辑最容易出错的地方4.1 账单生成策略按月还是按季别写死在代码里账单生成有两种常见做法签约时一次性把整个租期的账单全生成或者每月定时任务生成下月账单。前者简单但有个问题——租客提前退租后面没住的账单要作废逻辑麻烦后者灵活但依赖定时任务可靠性。我一般选前者因为课程设计里定时任务容易配错 cron 表达式而且一次性生成后账单列表查询简单不用每次动态算。生成逻辑按支付周期循环从start_date开始每次加一个月直到超过end_datepublic ListBill generateBills(Contract contract, BigDecimal monthlyRent) { ListBill bills new ArrayList(); LocalDate cursor contract.getStartDate(); int period 1; while (!cursor.isAfter(contract.getEndDate())) { Bill bill new Bill(); bill.setContractId(contract.getId()); bill.setAmount(monthlyRent); bill.setDueDate(cursor.plusDays(7)); // 每期开始后7天内付 bill.setPeriodNo(period); bill.setPayStatus(0); bills.add(bill); cursor cursor.plusMonths(1); } return bills; }plusMonths(1)会自动处理月末问题比如 1 月 31 日加一个月是 2 月 28 日非闰年不用自己判断。dueDate设成每期开始后 7 天给租客缓冲。批量插入用 MyBatis 的foreach一次插入别循环单条插几十条账单循环插会有明显延迟。4.2 合同到期与房源释放定时任务怎么写才不误伤合同到期后要做两件事把合同状态改成已结束把房源状态改回待租。用 Spring 的Scheduled每天凌晨跑一次Scheduled(cron 0 0 2 * * ?) public void handleExpiredContracts() { LocalDate today LocalDate.now(); ListContract expired contractMapper.selectExpired(today); for (Contract c : expired) { // 幂等已经是结束状态就跳过 if (c.getStatus() 2) { continue; } contractMapper.updateStatus(c.getId(), 2); // 只有房源还是已租状态才释放避免误改已下架的 houseMapper.updateStatus(c.getHouseId(), 1, 0); } }两个保护点一是幂等判断定时任务可能因为重试或手动触发跑两次状态已是结束的直接跳过二是释放房源时带旧状态条件updateStatus(houseId, 1, 0)只把「已租」改成「待租」如果房东已经手动下架状态 2不会被误改成待租。这个细节不做测试时手动改数据就会发现房源状态乱了。4.3 押金退还与账单核销金额计算别用 double退租时算押金常见逻辑是「押金 - 未付账单 - 损坏赔偿」。这里所有金额运算必须用BigDecimal而且要用setScale明确小数位public BigDecimal calculateRefund(Long contractId) { Contract c contractMapper.selectById(contractId); BigDecimal unpaid billMapper.sumUnpaidByContract(contractId); BigDecimal damage maintenanceMapper.sumDamageByContract(contractId); BigDecimal refund c.getDeposit() .subtract(unpaid) .subtract(damage) .setScale(2, RoundingMode.HALF_UP); return refund.max(BigDecimal.ZERO); // 不退负最多退到0 }setScale(2, RoundingMode.HALF_UP)四舍五入到分max(BigDecimal.ZERO)保证不会出现负数退款。用 double 算这些0.1 0.2 不等于 0.3 的问题会在对账时让你怀疑人生这是血泪经验。5. 避坑与排查那些让课程设计卡三天的坑5.1 房源状态并发更新导致重复签约现象两个租客同时点签约都成功了同一房源出现两份有效合同。原因先select查状态再update两个线程都查到状态 0都执行了更新。解决用带旧状态条件的更新UPDATE house SET status 1 WHERE id ? AND status 0判断影响行数为 0 就抛异常回滚。这个在前面signContract里已经体现但很多人写的时候会图省事写成先查后改。5.2 日期类型在 MyBatis 里映射错乱现象合同开始日期存进去是2024-01-01查出来变成2023-12-31 16:00:00。原因Java 8 的LocalDate和 MySQL 的DATE之间如果 MyBatis 版本低或者没配mybatis-typehandlers-jsr310会用默认的java.util.Date转换时区偏移 8 小时。解决MyBatis 3.4.5 以上自带 JSR310 支持确认依赖版本实体字段用LocalDate而不是DateJDBC URL 加serverTimezoneAsia/Shanghai。5.3 事务不生效导致签约一半成功现象签约时合同插入了但房源状态没改或者账单没生成。原因Transactional加在 private 方法上或者同类内部方法调用this 调用不走代理。解决事务方法必须是 public且从外部类调用。如果确实要在同类内调用注入自己或者用AopContext.currentProxy()。另外确认启动类上有EnableTransactionManagementSpring Boot 自动配了但手动配数据源时可能漏。5.4 分页查询总数和列表不一致现象列表显示 10 条但总数显示 8 条。原因总数查询和列表查询的 WHERE 条件不一致比如列表查的是status 0总数查的时候忘了加这个条件。解决把查询条件抽成一个HouseQuery对象列表和 count 两个 SQL 都引用同一套if条件或者用 MyBatis 插件自动生成 count 语句。手写两遍条件迟早会漏。5.5 密码明文存储被安全检查打回现象课程设计答辩时老师一看数据库密码是明文直接扣分。原因图省事直接存了。解决用 BCrypt 加密Spring Security 的BCryptPasswordEncoder单独用也行不一定要引入整个 Security。注册时encode登录时matches。BCrypt 每次加密结果不同但都能匹配不用自己加盐。6. 进阶技巧用状态机把房源流转管起来前面提到房源状态散落各处容易漏这里给一个具体做法。定义一个枚举和状态转移表public enum HouseStatus { AVAILABLE(0, 待租), RENTED(1, 已租), OFFLINE(2, 下架); private final int code; private final String desc; // 构造和 getter 省略 private static final MapHouseStatus, SetHouseStatus TRANSITIONS Map.of( AVAILABLE, Set.of(RENTED, OFFLINE), RENTED, Set.of(AVAILABLE), OFFLINE, Set.of(AVAILABLE) ); public boolean canTransferTo(HouseStatus target) { return TRANSITIONS.getOrDefault(this, Set.of()).contains(target); } }然后在 Service 里统一校验public void changeStatus(Long houseId, HouseStatus target) { House house houseMapper.selectById(houseId); HouseStatus current HouseStatus.of(house.getStatus()); if (!current.canTransferTo(target)) { throw new BizException(状态不允许从 current.getDesc() 转到 target.getDesc()); } houseMapper.updateStatus(houseId, current.getCode(), target.getCode()); }这样所有状态变更都走一个入口非法流转在编译期和运行期都能拦住。测试的时候可以写个单元测试遍历所有状态对验证转移表符合预期比手动点页面靠谱。验证方法上我习惯用 Postman 或 curl 跑一遍完整流程注册房东 → 发房源 → 注册租客 → 筛选房源 → 签约 → 查账单 → 模拟到期 → 查房源是否释放。每一步断言状态码和关键字段。这套流程跑通课程设计基本就稳了。最后说个我自己的习惯每次改完表结构一定同步更新建表 SQL 文件并提交别只在本地数据库改。答辩前老师要看数据库设计你本地改得乱七八糟但 SQL 文件是旧的现场重建就露馅。这个后悔药我吃过一次后来所有 DDL 都进版本控制。希望帮到你。本文还有配套的精品资源点击获取