☰
基于SSM框架的房屋租赁管理系统全流程开发实战
2026/10/21 18:05:34 网站建设 项目流程

我去年帮朋友打理一个小中介的门店时,看到他们墙上贴着几十张房源表,每张纸上密密麻麻都是手写备注,哪套房子租出去了、哪家住户的水电费还欠着,全靠脑子和本子记。那时候我就动了念头,索性用Java把这套流程做成一个房屋租赁管理系统。当时市面上讲SSM框架的资料不少,但要找一条从需求、建表、写接口、连着前端跑通全流程的完整路线,还真不是翻几篇博客就能拼出来。这篇博文把我自己完整写过的这个房屋租赁管理系统拆给你看,覆盖数据库设计、SSM三层架构、分页查询、角色权限、部署上线这些关键环节,适合正在做课程设计、毕业设计,或者刚学完Java Web想找个完整项目练手的同学直接照着做。

1. 为什么这个项目适合拿SSM来做:选题逻辑与功能边界

1.1 租赁管理到底要管什么——业务场景先想清楚

房屋租赁管理系统听起来简单,真正落到业务里,其实就是三类人的三个核心诉求:管理员要掌握全店房源状态,业务员要能登记新租客、签合同、收账单,租客(或者财务)要能查自己住了哪套房、交过多少钱。我的项目把范围控制在"一套房从挂出、出租、收租到退租"这条主线上,具体拆成六个功能块:

  • 登录认证:区分管理员和普通业务员两种角色,没登录的人一律进不了后台。
  • 房源管理:新增房源、修改信息、上下架状态(待租/已租/维修),配套图片上传。
  • 租客登记:录入租客姓名、电话、身份证号,作为合同签订的基本档案。
  • 合同管理:将房源和租客关联起来,记录起租日期、到期日期、月租金、押金。
  • 账单管理:按合同生成租金账单,还可以附加水费、电费等杂费,标注已收/未收。
  • 仪表盘统计:用简单SQL统计在租房源数、本月应收金额、合同到期预警。

很多人在做这个选题时会犯一个错,就是把系统无限扩大,今天加个报修工单,明天加个员工排班,结果数据库十几个表、页面几十个,最后演示都来不及跑完。我建议你把边界圈小一点,先保证主线闭环,再谈扩展。

1.2 SSM在今天依然值得手写一遍的原因

现在很多新项目一上来就是Spring Boot,但我的经验是:把SSM(Spring + Spring MVC + MyBatis)完整搭一遍,你对Java Web框架的理解会通透很多。Spring Boot把XML配置、依赖管理、自动装配都封装好了,你用起来很快,但一旦出问题会觉得很飘,说不清楚请求是怎么一步步走到数据库的。而SSM是完全手动装配的,你要自己写web.xml、自己配置DispatcherServlet、自己声明数据源和事务管理器。

这个项目恰好适合SSM的另一个原因是,租赁管理天然适合MyBatis的多表关联查询。房源、租客、合同、账单之间关系复杂,MyBatis的动态SQL能让你在查询条件不确定时灵活拼SQL,这一点在后面的开发里会反复用到。再加上PageHelper分页插件本身就是为MyBatis设计的,配合Mapper接口几乎零侵入。

1.3 建议的技术选型和功能取舍

我在开发时直接固定了这样的技术组合:JDK 1.8 + Maven 3.6 + Tomcat 8.5 + MySQL 5.7,框架用Spring 5.0系列、Spring MVC、MyBatis 3.5系列,前端不引入Vue那种重框架,而是用JSP加上少量JavaScript和CSS。这么做不是因为前端技术旧,而是因为SSM项目最常见的部署方式就是WebContent里放JSP页面,war包往Tomcat一扔就能跑,对新手最友好。

前端页面方面,我用了半开源的后台模板(某开源Admin模板,只做样式层),自己只写业务页面和数据交互。项目结构上严格分层:Controller只管参数接收和页面跳转,Service处理业务逻辑,Mapper只做数据库CRUD,这样后面排查问题的时候每层职责非常清楚。

2. 数据库表设计的核心思路:五张表撑起租赁闭环

2.1 表结构和字段详解

租赁系统的数据库设计是整个项目里最值得花时间琢磨的部分。我把它抽象成了五张核心表,下面是经过实际调整后的建表SQL,你可以直接参考:

CREATE TABLE `t_user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `real_name` VARCHAR(30), `phone` VARCHAR(20), `role` TINYINT DEFAULT 2 COMMENT '1-管理员 2-业务员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `t_house` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL, `address` VARCHAR(255) NOT NULL, `area` DECIMAL(8,2), `price` DECIMAL(10,2) NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0-待租 1-已租 2-维修', `owner_id` INT, `cover` VARCHAR(255), `description` TEXT, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `t_customer` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `phone` VARCHAR(20) NOT NULL, `id_card` VARCHAR(30), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `t_contract` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `house_id` INT NOT NULL, `customer_id` INT NOT NULL, `start_date` DATE NOT NULL, `end_date` DATE NOT NULL, `monthly_rent` DECIMAL(10,2) NOT NULL, `deposit` DECIMAL(10,2) DEFAULT 0, `status` TINYINT DEFAULT 1 COMMENT '1-生效中 2-已退租', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `t_bill` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `contract_id` INT NOT NULL, `bill_type` TINYINT DEFAULT 1 COMMENT '1-租金 2-水费 3-电费', `amount` DECIMAL(10,2) NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0-未收 1-已收', `bill_date` DATE, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP );

2.2 为什么要把状态字段做成数字枚举而不是字符串

一开始我把房源的"待租/已租"存成中文,当时觉得直观,后来发现两个问题:一是拼接查询条件时写status = '已租'很容易因为前后端编码不一致出乱子;二是扩展麻烦,如果以后要加"已锁定"状态,字符串语义不清晰。改成数字枚举后,页面显示时再做翻译,比如VO层用Map或字典表做映射,查询效率高,后端判断也简单。

合同表我特别加了deposit押金字段,别小看这个字段,很多系统在签合同时要记录押金,但这个押金最终在退租时可能退还也可能抵扣欠费,如果和月租金混在一个字段里,后面对账根本说不清。物理上分开存储,逻辑上清清楚楚。

2.3 合同表是连接房源和租客的桥梁

从业务上看,合同表是整个系统里最关键的一张表。房源和租客之间是多对多的关系,如果不加中间表直接改t_house里的customer_id,就会丢失历史记录——上一任租客的信息直接被覆盖了。合同表的存在实际上是一种关联实体的正确处理方式:一套房源可以有多条合同记录,但同一时间只有一条生效中的合同;一个租客也可以有多条历史合同。查询"某套房现在谁在住",本质就是查status=1且house_id=某房的合同对应的客户。

账单表挂到contract_id而不是直接挂house_id,也是为了保持口径统一。因为租金金额、起止日期都在合同里定了,账单从合同派生出来最不容易出偏差。水电费这种变量费用则通过bill_type区分,再提供给业务员手动录入。

2.4 数据库设计的几个小细节

给关键查询字段加索引,特别是t_contract的house_id、status组合查询很频繁,我建了联合索引idx_house_status。另外,所有表都保留了create_time,新增记录时直接用DEFAULT CURRENT_TIMESTAMP,这对仪表盘统计"本月新增房源数"非常方便,不用额外在代码里处理时间。项目里还加了一张t_dictionary表备用,存储支付方式、房源户型等可扩展的字典项,算是给后续需求留了口子。

3. 项目搭建与核心模块实现:从依赖配置到业务代码

3.1 用Maven搭建SSM骨架,关注这几个依赖

我是用Maven创建的war工程,这比在Eclipse里手动添加jar包靠谱得多。核心依赖集中在spring-context、spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java、javax.servlet-api、jstl、fastjson(或者jackson-databind)以及pagehelper。这里重点说一下pagehelper,它要为Spring和MyBatis同时工作,你需要这样配置:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.11</version> </dependency>

然后在MyBatis配置里追加插件:

<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="helperDialect" value="mysql"/> <property name="reasonable" value="true"/> </plugin> </plugins>

helperDialect=mysql是为了让分页插件生成MySQL的LIMIT语句,reasonable=true意思是当你请求第0页或者超出总页数时自动修正到合理页,这个对前端页码越界的情况很友好,建议开着。

3.2 Spring MVC请求链路和配置文件的关键配置

SSM最容易被新手弄晕的就是三个配置文件各自管什么。我的经验是一句话概括:spring-mvc.xml管Controller和视图,spring-mybatis.xml管Service、数据源和事务,web.xml负责把这两个配置加载进Servlet容器。

在spring-mvc.xml里,除了常规的<context:component-scan base-package="com.demo.*.controller"/>和视图解析器外,有几个细节非常关键。第一是静态资源放行,JSP引用的CSS、JS、图片会被DispatcherServlet拦截,必须加<mvc:default-servlet-handler/>和<mvc:resources location="/static/" mapping="/static/**"/>,不配置这行你的页面样式全都会挂掉。第二是JSON转换,Controller返回@ResponseBody时,我直接用的Fastjson的FastJsonHttpMessageConverter,对象转JSON省心。

spring-mybatis.xml里最重要的是数据源和事务。我用的阿里Druid连接池,initialSize=5、maxActive=20,配置完成后记得验证testWhileIdle=true。事务管理器绑定到DataSourceTransactionManager,然后<tx:annotation-driven transaction-manager="transactionManager"/>开启注解事务。

3.3 手写租客管理模块:一个Controller和一页列表看懂SSM三层

我用租客管理模块来演示最典型的"查询列表+新增"流程。先看Controller部分:

@Controller @RequestMapping("/customer") public class CustomerController { @Resource private CustomerService customerService; @RequestMapping("/list") public String list(Model model, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { PageHelper.startPage(pageNum, pageSize); List<CustomerVO> list = customerService.queryList(keyword); PageInfo<CustomerVO> pageInfo = new PageInfo<>(list); model.addAttribute("pageInfo", pageInfo); return "customer/list"; } @ResponseBody @RequestMapping("/save") public Result save(@RequestBody Customer customer) { if (StringUtils.isBlank(customer.getName()) || StringUtils.isBlank(customer.getPhone())) { return Result.error("姓名和电话不能为空"); } customerService.addCustomer(customer); return Result.ok(); } }

注意这里我用PageHelper.startPage和return "customer/list"配合,PageHelper会拦截紧接着执行的一条MyBatis查询,自动分页。PageInfo里封装了总条数、总页数、当前页等所有分页信息,前端直接用就行。新增业务的校验我放在Controller入口做了,简单数据直接拦,复杂的业务校验再下沉到Service。

Service层的两个方法,addCustomer标了@Transactional,因为后续要同时维护客户档案和客户来源日志表(这个项目里简化成一张表就直接插),保证要么都成功要么都回滚。还有一个容易被忽略的点:Controller返回字符串走视图解析器,返回对象走JSON,别搞混。当方法上写了@ResponseBody时,返回Result对象会被Fastjson序列化成{"code":200,"msg":"ok"},供前端异步调用。

3.4 MyBatis动态SQL:房源多条件查询的关键

房源列表是系统使用频率最高的页面,通常有"按标题模糊搜、按状态筛选、按租金区间筛选"这几个条件。这种不确定组合的条件,用MyBatis动态SQL最舒服。我把查询方法写在HouseMapper.java接口里,对应的XML核心片段如下:

<select id="queryByCondition" parameterType="map" resultType="com.demo.entity.House"> SELECT h.*, r.real_name AS owner_name FROM t_house h LEFT JOIN t_user r ON h.owner_id = r.id <where> <if test="keyword != null and keyword != ''"> AND h.title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="status != null"> AND h.status = #{status} </if> <if test="minPrice != null"> AND h.price &gt;= #{minPrice} </if> <if test="maxPrice != null"> AND h.price &lt;= #{maxPrice} </if> </where> ORDER BY h.create_time DESC </select>

<where>标签会自动去掉开头的AND,这个细节能帮你省掉很多麻烦。注意XML里的>、<必须用&gt;、&lt;转义,否则XML解析直接报错。resultType直接映射到House实体,我额外查了一个owner_name字段,所以实体里加了ownerName属性,通过下划线转驼峰映射过去,MyBatis的mapUnderscoreToCamelCase配置一定要设为true,否则owner_name根本赋值不上。

3.5 合同签订和退租的业务场景实现

签合同这个操作涉及两张表:往t_contract插一条新合同记录,同时把t_house的状态改成已租。我把这两个操作放在一个事务方法里:

@Transactional(rollbackFor = Exception.class) public void signContract(Contract contract) { // 校验房源当前状态必须为待租 House house = houseMapper.selectById(contract.getHouseId()); if (house.getStatus() != HouseStatus.AVAILABLE) { throw new BusinessException("该房源当前不可出租"); } contractMapper.insert(contract); houseMapper.updateStatus(contract.getHouseId(), HouseStatus.RENTED); // 自动生成首期租金账单 Bill bill = new Bill(); bill.setContractId(contract.getId()); bill.setAmount(contract.getMonthlyRent()); bill.setBillType(1); bill.setBillDate(new Date()); billMapper.insert(bill); }

退租则是反向操作:合同状态改为已退租,房源状态改回待租。这块一定要放在事务里,而且要注意规范的方法粒度,别把@Transactional加到Controller方法上,事务是面向Service方法的。还有个小坑是Spring的自调用问题:如果同一个类里方法A调方法B,B上的@Transactional不会生效,所以我把事务边界尽量放在Service公开方法上。

4. 前端接入与权限控制:实际开发里最容易翻车的几个地方

4.1 后台页面用JSP + 模板,接口返回JSON,前端自己渲染

这个项目我选择了JSP作为视图层,页面结构类似"顶部菜单 + 左侧导航 + 主内容区"的经典后台布局。列表页用手写表格和简单的JavaScript渲染数据。例如租客列表我直接用JSTL循环:

<c:forEach items="${pageInfo.list}" var="c"> <tr> <td>${c.id}</td> <td>${c.name}</td> <td>${c.phone}</td> <td><fmt:formatDate value="${c.createTime}" pattern="yyyy-MM-dd"/></td> </tr> </c:forEach>

需要强调的坑是JSP页面上的日期格式化。如果你的实体里createTime是java.util.Date,直接用${c.createTime}输出会变成很难看的英文格式,必须用fmt:formatDate。而JSON接口返回给前端时,Fastjson默认把Date序列化成时间戳数字,如果你在页面写new Date(时间戳)还好,直接展示就完全不对了。我的做法是在实体类的日期字段上加了@JSONField(format = "yyyy-MM-dd HH:mm:ss"),这样接口返回的就是字符串,前端拿过来直接显示,省事很多。

4.2 登录和角色权限控制:Session拦截器与按钮权限

权限控制是这个项目的一道坎。我用的是最直接有效的方案:自定义一个HandlerInterceptor,在preHandle里查Session。

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } // 额外的管理员校验 if (request.getRequestURI().contains("/admin/") && user.getRole() != 1) { response.setStatus(403); return false; } return true; } }

在spring-mvc.xml里注册时,要注意把登录接口、静态资源排除掉:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.demo.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>

我还额外处理了管理员专属操作。比如删除合同、删除房源这种高风险操作,URL统一放在/admin/下,拦截器里再判断一次用户角色。注意session里的用户对象要保存完整,不只是用户名,否则每次都要查库。实际踩过的坑是改了用户角色后Session里的旧角色还在,所以登录时重新setAttribute,用户信息变更后也必须同步更新Session。

4.3 分页插件与多表查询的count语句冲突

这个坑我印象特别深。在房源列表页做分页时,我的查询SQL里有LEFT JOIN,还带了ORDER BY,PageHelper在生成count查询时会保留ORDER BY,如果查询里有复杂表达式就可能报错。现象是列表第一页正常,点第二页时直接SQL异常。排查链路是这样的:

  • 第一步:看控制台输出,发现报错来自SELECT count(0)片段。
  • 第二步:单独把PageHelper生成的count SQL拿到Navicat执行,复现确实报错。
  • 第三步:原因是count语句里跟着ORDER BY,对连表查询会引发报错。
  • 第四步:解决方案是确保SQL写法足够规整,必要时用@Options(useGeneratedKeys = true)无关,关键是不要在多表查询里用不确定的排序字段,或者升级PageHelper到5.1.11以上让count查询自动剥离不必要的部分。

这个问题的本质是分页插件不能智能判断哪些部分对count无用,所以如果你在SQL里写了ORDER BY (CASE WHEN ...)这类表达式,插件在count时就会很被动。遇到这个情况,最稳妥的方案是单独写一个不带排序的count查询,或者把复杂排序放到查询条件外面。

4.4 删除数据与外键约束之间的矛盾

刚开始我把房源删除设计成物理删除,就是直接DELETE FROM t_house WHERE id=?。结果删除一个已经签过合同的房源时,因为合同表有外键关联,直接被数据库拦下,报"外键约束失败"。我当时的第一反应是否掉外键,但转念一想这样做会把历史数据搞丢。

正确做法是把删除改成逻辑删除或状态限制:房源状态是"已租"时,不提供删除按钮;只有"待租"状态的才能删除,而且删除也是做一次UPDATE t_house SET deleted=1,查询时强制过滤。这样既保住了数据完整性,又不会报错。同理,租客删除前也要先查有没有生效中的合同。

5. 本机跑通到部署上线:一次完整的SSM项目发布记录

5.1 从IDEA到Tomcat的完整运行过程

为了让项目能给别人直接演示,我把自己跑通的流程固定下来了。在IDEA里配置Tomcat时,要选war包方式而不是war exploded,这样部署行为更接近上线。部署名可以设置成/house-rent,访问地址就是http://localhost:8080/house-rent/。数据库初始化时,我用一个init.sql脚本,里面包含建库建表语句和初始账号数据,管理员用户名admin、密码经过MD5加密存储,业务员账号staff01。

这里有个细节值得说:登录密码存库前我做了MD5加盐处理,盐值存同一个用户的salt字段里,登录时先把用户输入密码加盐再MD5比对。别用明文密码,也别不加盐直接MD5,同一密码不加盐会生成相同哈希,很容易被撞库。在Java里我用DigestUtils.md5Hex(password + salt)一行搞定。

5.2 打成war包丢进Tomcat的部署细节

演示完成后,我执行mvn clean package生成house-rent.war,把它丢进Tomcat的webapps目录,启动Tomcat后会自动解压部署。这里有几个检查项:

  • JDK版本和Tomcat版本要兼容,JDK8对应Tomcat8.5或9,JDK11就要考虑Tomcat9以上。
  • 数据库连接地址要改成部署机器能访问的地址,别写localhost就真连不上远程库。
  • 服务器防火墙要放行8080端口。
  • 如果项目用了<url-pattern>/</url-pattern>这种全路径拦截,一定要配好静态资源放行,否则页面CSS全挂。

我实际部署时遇到的另一个坑是tomcat的server.xml里URIEncoding没配置,导致URL里带中文参数时乱码。在Connector节点加URIEncoding="UTF-8"解决。

5.3 上线后要根据使用反馈做的几件事

项目跑通只是起点。接了几个月的真实使用反馈后,我发现有三处值得迭代:一是租房合同到期前30天要提醒业务员联系续签,可以在仪表盘做一个"即将到期"列表;二是账单明细最好支持导出Excel,财务对账要用;三是房源图片尽量不要多张大图直接存Tomcat目录,不然磁盘和访问性能很快告急,可以放到专门的静态文件服务或者对象存储里。这些需求都不难,在现有架构上逐个加接口和页面就行。

6. 完成后回头看:这套SSM租赁系统的真实价值

把整个系统做完之后,我最大的体会是SSM项目的难点从来不在某一个框架,而在于三件事:数据库设计是否贴合业务实际、三层之间职责是否清晰、还有那些不起眼的配置细节是否到位。比如你花一小时写的业务代码可能就几十行,但排查一个Fastjson序列化Date的问题可能就要一下午。我建议你在复现我这个项目时,不要急着把代码敲完就结束,可以试着给自己提几个问题:如果租客提前退租,押金和账单怎么改?如果一套房单价调整了,生效中的合同是否要变?这些问题的答案会倒逼你优化表结构和代码设计。

我给想做这个项目的朋友一个具体建议:先把数据库建模和页面原型画清楚,再去写代码。磨刀不误砍柴工。别看这些设计工作不产出代码,但在后面会帮你避开无数个"表字段不够用""接口参数对不上"的大坑。如果时间紧,可以先按我上面的五张表把主线做通,以后再慢慢扩展。祝你也能跑通一个属于自己的房屋租赁管理系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询