简介:本资源为基于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 或者生僻字。连接池用 HikariCP,Spring Boot 默认就带,不用额外配。
2.2 核心实体与表结构:五张表撑起最小闭环
先把领域模型定下来,后面写代码才不会反复改表。最小闭环需要这几张表:
| 表名 | 作用 | 关键字段 |
|---|---|---|
user | 用户(房东/租客/管理员) | id, username, password, role, phone |
house | 房源 | id, landlord_id, title, address, price, status, area, room_type |
contract | 租赁合同 | id, house_id, tenant_id, start_date, end_date, deposit, status |
bill | 账单 | id, contract_id, amount, due_date, pay_status |
maintenance | 维修工单 | id, house_id, tenant_id, description, status |
house.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)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;三个细节:第一,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 塞进 ThreadLocal,Service 层直接取。
3.3 多条件筛选房源:动态 SQL 怎么写才不翻车
筛选是租赁系统的门面,用户会同时传区域、价格区间、户型、排序方式。MyBatis 的<if>标签处理这种场景很顺手:
<select id="selectByCondition" resultType="HouseVO"> SELECT h.*, u.username AS landlordName FROM house h LEFT JOIN user u ON h.landlord_id = u.id <where> h.status = 0 <if test="address != null and address != ''"> AND h.address LIKE CONCAT('%', #{address}, '%') </if> <if test="minPrice != null"> AND h.price >= #{minPrice} </if> <if test="maxPrice != null"> AND h.price <= #{maxPrice} </if> <if test="roomType != null and roomType != ''"> AND h.room_type = #{roomType} </if> </where> ORDER BY <choose> <when test="sort == 'priceAsc'">h.price ASC</when> <when test="sort == 'priceDesc'">h.price DESC</when> <otherwise>h.create_time DESC</otherwise> </choose> LIMIT #{offset}, #{pageSize} </select>注意<where>标签会自动去掉开头多余的 AND,不用手写WHERE 1=1。排序用<choose>而不是直接拼字符串,因为${}拼接有 SQL 注入风险,虽然这里 sort 是枚举值可控,但养成用<choose>的习惯更稳。分页参数offset和pageSize在 Service 层算好传进来,offset = (pageNum - 1) * pageSize。
提示:
LIKE CONCAT('%', #{address}, '%')这种前置模糊匹配用不上索引,数据量上万后会明显变慢。课程设计阶段数据量小无所谓,但如果想优化,可以改成全文索引或者引入 Elasticsearch,不过那是另一个话题了。
4. 合同、账单与到期处理:业务逻辑最容易出错的地方
4.1 账单生成策略:按月还是按季,别写死在代码里
账单生成有两种常见做法:签约时一次性把整个租期的账单全生成,或者每月定时任务生成下月账单。前者简单但有个问题——租客提前退租,后面没住的账单要作废,逻辑麻烦;后者灵活但依赖定时任务可靠性。我一般选前者,因为课程设计里定时任务容易配错 cron 表达式,而且一次性生成后账单列表查询简单,不用每次动态算。
生成逻辑按支付周期循环,从start_date开始,每次加一个月,直到超过end_date:
public List<Bill> generateBills(Contract contract, BigDecimal monthlyRent) { List<Bill> 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(); List<Contract> 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而不是Date;JDBC URL 加serverTimezone=Asia/Shanghai。
5.3 事务不生效导致签约一半成功
现象:签约时合同插入了,但房源状态没改,或者账单没生成。原因:@Transactional加在 private 方法上,或者同类内部方法调用(this 调用)不走代理。解决:事务方法必须是 public,且从外部类调用。如果确实要在同类内调用,注入自己或者用AopContext.currentProxy()。另外确认启动类上有@EnableTransactionManagement(Spring 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 Map<HouseStatus, Set<HouseStatus>> 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 都进版本控制。希望帮到你。
本文还有配套的精品资源,点击获取