资讯动态

基于SSM框架的房屋租赁管理系统全流程开发实战

发布时间:2026/10/21 18:05:53 来源:尧图企业网站定制
我去年帮朋友打理一个小中介的门店时看到他们墙上贴着几十张房源表每张纸上密密麻麻都是手写备注哪套房子租出去了、哪家住户的水电费还欠着全靠脑子和本子记。那时候我就动了念头索性用Java把这套流程做成一个房屋租赁管理系统。当时市面上讲SSM框架的资料不少但要找一条从需求、建表、写接口、连着前端跑通全流程的完整路线还真不是翻几篇博客就能拼出来。这篇博文把我自己完整写过的这个房屋租赁管理系统拆给你看覆盖数据库设计、SSM三层架构、分页查询、角色权限、部署上线这些关键环节适合正在做课程设计、毕业设计或者刚学完Java Web想找个完整项目练手的同学直接照着做。1. 为什么这个项目适合拿SSM来做选题逻辑与功能边界1.1 租赁管理到底要管什么——业务场景先想清楚房屋租赁管理系统听起来简单真正落到业务里其实就是三类人的三个核心诉求管理员要掌握全店房源状态业务员要能登记新租客、签合同、收账单租客或者财务要能查自己住了哪套房、交过多少钱。我的项目把范围控制在一套房从挂出、出租、收租到退租这条主线上具体拆成六个功能块登录认证区分管理员和普通业务员两种角色没登录的人一律进不了后台。房源管理新增房源、修改信息、上下架状态待租/已租/维修配套图片上传。租客登记录入租客姓名、电话、身份证号作为合同签订的基本档案。合同管理将房源和租客关联起来记录起租日期、到期日期、月租金、押金。账单管理按合同生成租金账单还可以附加水费、电费等杂费标注已收/未收。仪表盘统计用简单SQL统计在租房源数、本月应收金额、合同到期预警。很多人在做这个选题时会犯一个错就是把系统无限扩大今天加个报修工单明天加个员工排班结果数据库十几个表、页面几十个最后演示都来不及跑完。我建议你把边界圈小一点先保证主线闭环再谈扩展。1.2 SSM在今天依然值得手写一遍的原因现在很多新项目一上来就是Spring Boot但我的经验是把SSMSpring 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就会丢失历史记录——上一任租客的信息直接被覆盖了。合同表的存在实际上是一种关联实体的正确处理方式一套房源可以有多条合同记录但同一时间只有一条生效中的合同一个租客也可以有多条历史合同。查询某套房现在谁在住本质就是查status1且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 groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.1.11/version /dependency然后在MyBatis配置里追加插件plugins plugin interceptorcom.github.pagehelper.PageInterceptor property namehelperDialect valuemysql/ property namereasonable valuetrue/ /plugin /pluginshelperDialectmysql是为了让分页插件生成MySQL的LIMIT语句reasonabletrue意思是当你请求第0页或者超出总页数时自动修正到合理页这个对前端页码越界的情况很友好建议开着。3.2 Spring MVC请求链路和配置文件的关键配置SSM最容易被新手弄晕的就是三个配置文件各自管什么。我的经验是一句话概括spring-mvc.xml管Controller和视图spring-mybatis.xml管Service、数据源和事务web.xml负责把这两个配置加载进Servlet容器。在spring-mvc.xml里除了常规的context:component-scan base-packagecom.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连接池initialSize5、maxActive20配置完成后记得验证testWhileIdletrue。事务管理器绑定到DataSourceTransactionManager然后tx:annotation-driven transaction-managertransactionManager/开启注解事务。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); ListCustomerVO list customerService.queryList(keyword); PageInfoCustomerVO 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 idqueryByCondition parameterTypemap resultTypecom.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 testkeyword ! null and keyword ! AND h.title LIKE CONCAT(%, #{keyword}, %) /if if teststatus ! null AND h.status #{status} /if if testminPrice ! null AND h.price gt; #{minPrice} /if if testmaxPrice ! null AND h.price lt; #{maxPrice} /if /where ORDER BY h.create_time DESC /selectwhere标签会自动去掉开头的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调方法BB上的Transactional不会生效所以我把事务边界尽量放在Service公开方法上。4. 前端接入与权限控制实际开发里最容易翻车的几个地方4.1 后台页面用JSP 模板接口返回JSON前端自己渲染这个项目我选择了JSP作为视图层页面结构类似顶部菜单 左侧导航 主内容区的经典后台布局。列表页用手写表格和简单的JavaScript渲染数据。例如租客列表我直接用JSTL循环c:forEach items${pageInfo.list} varc tr td${c.id}/td td${c.name}/td td${c.phone}/td tdfmt:formatDate value${c.createTime} patternyyyy-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 classcom.demo.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors我还额外处理了管理员专属操作。比如删除合同、删除房源这种高风险操作URL统一放在/admin/下拦截器里再判断一次用户角色。注意session里的用户对象要保存完整不只是用户名否则每次都要查库。实际踩过的坑是改了用户角色后Session里的旧角色还在所以登录时重新setAttribute用户信息变更后也必须同步更新Session。4.3 分页插件与多表查询的count语句冲突这个坑我印象特别深。在房源列表页做分页时我的查询SQL里有LEFT JOIN还带了ORDER BYPageHelper在生成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 deleted1查询时强制过滤。这样既保住了数据完整性又不会报错。同理租客删除前也要先查有没有生效中的合同。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或9JDK11就要考虑Tomcat9以上。数据库连接地址要改成部署机器能访问的地址别写localhost就真连不上远程库。服务器防火墙要放行8080端口。如果项目用了url-pattern//url-pattern这种全路径拦截一定要配好静态资源放行否则页面CSS全挂。我实际部署时遇到的另一个坑是tomcat的server.xml里URIEncoding没配置导致URL里带中文参数时乱码。在Connector节点加URIEncodingUTF-8解决。5.3 上线后要根据使用反馈做的几件事项目跑通只是起点。接了几个月的真实使用反馈后我发现有三处值得迭代一是租房合同到期前30天要提醒业务员联系续签可以在仪表盘做一个即将到期列表二是账单明细最好支持导出Excel财务对账要用三是房源图片尽量不要多张大图直接存Tomcat目录不然磁盘和访问性能很快告急可以放到专门的静态文件服务或者对象存储里。这些需求都不难在现有架构上逐个加接口和页面就行。6. 完成后回头看这套SSM租赁系统的真实价值把整个系统做完之后我最大的体会是SSM项目的难点从来不在某一个框架而在于三件事数据库设计是否贴合业务实际、三层之间职责是否清晰、还有那些不起眼的配置细节是否到位。比如你花一小时写的业务代码可能就几十行但排查一个Fastjson序列化Date的问题可能就要一下午。我建议你在复现我这个项目时不要急着把代码敲完就结束可以试着给自己提几个问题如果租客提前退租押金和账单怎么改如果一套房单价调整了生效中的合同是否要变这些问题的答案会倒逼你优化表结构和代码设计。我给想做这个项目的朋友一个具体建议先把数据库建模和页面原型画清楚再去写代码。磨刀不误砍柴工。别看这些设计工作不产出代码但在后面会帮你避开无数个表字段不够用接口参数对不上的大坑。如果时间紧可以先按我上面的五张表把主线做通以后再慢慢扩展。祝你也能跑通一个属于自己的房屋租赁管理系统。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑