简介:本资源是一套完整的基于SpringBoot开发的房屋租赁系统毕业设计项目,面向计算机专业本科生及Java Web初学者,解决课程设计、毕设选题与全栈开发实践需求。压缩包共4个文件,含可运行源码(ZIP)、数据库脚本(SQL)、万字技术文档(DOC)及简要说明(TXT),整体23.96MB,结构清晰、开箱即用。已有46人学习下载,适用于快速掌握SpringBoot+MyBatis+Thymeleaf主流技术栈在真实业务场景中的集成应用。读者可直接部署运行,完整体验用户、房东、管理员三角色协同的租赁全流程,包括房源发布审核、预约看房管理、订单支付状态跟踪及后台多维度数据维护,并获得规范化的系统设计思路、数据库ER模型、前后端模块划分逻辑与文档撰写范式。
1. 项目概述:一个“麻雀虽小,五脏俱全”的实战演练场
看到“基于SpringBoot的房屋租赁系统”这个标题,很多开发者,尤其是刚学完SpringBoot基础、想找个项目练手的朋友,眼睛肯定会一亮。这确实是一个经典到不能再经典的“毕业设计级”或“个人练手级”项目选题。它听起来很接地气,似乎就是把我们日常生活中租房、找房的那套流程搬到线上。但千万别小看它,这个项目就像一个微缩的“商业世界”,里面几乎涵盖了后端开发工程师日常工作中80%的核心技能点:用户权限管理、复杂业务逻辑(房源上下架、预约看房、合同生成)、数据库设计、前后端交互、乃至一些简单的安全考量。我之所以说它是个绝佳的实战演练场,是因为它需求明确、边界清晰,但又足够让你把SpringBoot那一套技术栈(Spring MVC, Spring Data JPA/MyBatis, Spring Security等)从头到尾串起来用一遍。你得到的不仅仅是一份能跑起来的源码和一个数据库脚本,更重要的是一套解决典型业务问题的完整思路和实现路径。对于初学者,这是从理论到实践的桥梁;对于有经验的开发者,这也是一个检验自己架构设计、代码规范能力的试金石。
2. 系统核心需求与业务逻辑拆解
在动手敲代码之前,我们必须把“房屋租赁”这件事背后的业务逻辑彻底理清。一个好的系统源于对业务的深刻理解,而不是技术的简单堆砌。
2.1 角色与功能矩阵
任何涉及交易和管理的系统,首要任务就是划分角色。一个典型的房屋租赁系统至少包含三类核心用户,他们的诉求和权限截然不同:
- 租客:核心诉求是“找到好房”。他们的功能流是:注册/登录 -> 浏览/搜索房源 -> 查看房源详情(图片、描述、价格、位置)-> 收藏感兴趣房源 -> 在线联系房东或提交看房申请 -> 在线上签约(如果系统支持)-> 支付租金/押金 -> 合同期内可能发起维修申请或续租/退租流程。
- 房东/房源发布者:核心诉求是“把房快速租出去,管理好租务”。他们的功能流是:注册/登录(需更严格的身份审核,如实名认证)-> 发布房源(填写详细信息、上传图片)-> 管理已发布房源(上下架、编辑)-> 处理租客的看房预约或咨询 -> 在线与意向租客沟通、确认合同 -> 管理租约状态(收款、到期提醒)-> 处理租客的维修反馈。
- 系统管理员:核心诉求是“维持平台秩序与安全”。他们是系统的“上帝视角”,功能包括:管理所有用户账户(审核、封禁)-> 审核房东身份和房源信息(防止虚假信息)-> 管理全站房源(可强制下架违规房源)-> 查看全站数据报表(用户数、房源数、交易量)-> 配置系统参数(如手续费率、公告信息)。
注意:在实际设计中,房东和管理员的权限边界需要仔细界定。例如,房源审核是管理员执行,但审核通过后的日常上下架操作应归属房东。这需要在后端接口权限(
@PreAuthorize)和前端菜单渲染上做精细控制。
2.2 核心业务状态机
业务逻辑的核心是“状态”的变化。以一套房源的生命周期为例,它绝非简单的“存在”或“不存在”,而是一个动态流转的过程:
- 草稿状态:房东刚创建,信息未填完,或存为草稿。
- 待审核状态:房东提交发布申请,等待管理员审核。
- 已上架/可租状态:审核通过,对所有租客可见。
- 已预订/锁定状态:有租客提交了看房申请或支付了定金,在此期间其他租客可能只能查看但无法操作预订。
- 已出租状态:租约正式生效,合同期内房源应从可租列表中隐藏,或标记为“已租”。
- 已下架状态:房东主动下架或管理员因违规操作下架。
每一个状态变迁都对应着后端的一个服务方法,并且可能触发一系列副作用,例如:状态变为“已出租”时,需要生成电子合同、创建租金支付计划、向双方发送通知等。用代码实现时,状态模式或至少是清晰的枚举类(Enum)配合数据库状态字段是必不可少的。
2.3 非功能性需求考量
除了“做什么”,我们还得想想“做得好不好”。对于这个练手项目,以下几方面能显著提升其完整性和面试时的说服力:
- 数据一致性:例如,租客支付租金时,既要更新订单状态,又要生成流水记录,这需要用到Spring的
@Transactional注解来保证事务。 - 简单搜索与筛选:这是租客最常用的功能。后端不能只是
select * from house,而要支持基于地段、价格区间、户型、关键词的复合查询。这里会用到JPA的Specification或MyBatis的动态SQL。 - 图片上传与管理:房源图片是重中之重。你需要决定是存储在服务器本地,还是使用云存储服务(如OSS)。这涉及到文件上传接口、图片压缩、防盗链等知识。
- 基础的安全性:用户密码必须加盐哈希存储(使用BCryptPasswordEncoder);敏感操作(如删除房源、修改金额)必须有权限校验和日志记录;防止SQL注入(使用预编译语句,MyBatis的
#{})和基础的XSS过滤。
3. 技术栈选型与架构设计思路
基于SpringBoot,我们有丰富的技术组件可以选择。选型没有绝对的对错,但需要有合理的理由。以下是我为这个项目推荐的一套稳健、主流且易于学习的组合。
3.1 后端技术栈详解
- 核心框架:Spring Boot 2.7.x:选择这个相对成熟稳定的版本,而非最新的3.x,是为了避免在学习和部署过程中遇到一些因版本过新而导致的冷门依赖兼容性问题。它能让我们快速搭建、自动配置,专注于业务。
- 数据持久层:Spring Data JPA + Hibernate:为什么选JPA而不是更灵活的MyBatis?对于这个业务模型相对固定的租赁系统,JPA的ORM(对象关系映射)能力可以极大减少手写SQL的工作量。通过定义
House、User、Order等实体类,并配置好关联关系(@OneToMany,@ManyToOne),JPA能帮我们自动处理很多关联查询。而且,它的Repository接口写法非常简洁,适合快速开发。- 实操心得:在实体类设计时,强烈建议使用
Long类型的id并搭配@GeneratedValue策略,同时为所有表添加create_time和update_time(可用@CreationTimestamp和@UpdateTimestamp自动填充)。这为后续排查问题和数据分析提供了极大便利。
- 实操心得:在实体类设计时,强烈建议使用
- 数据库:MySQL 8.0:关系型数据库是不二之选。MySQL社区活跃、资料丰富,且完全能满足此系统的数据规模和并发要求。使用8.0版本可以体验更好的性能和一些新特性(如窗口函数,用于复杂的统计报表)。
- 权限安全:Spring Security + JWT:这是实现多角色权限系统的核心。Spring Security负责整个Web请求的安全过滤链。我们采用无状态的JWT(JSON Web Token)方案,而非传统的Session。用户登录成功后,后端生成一个包含用户ID和角色信息的JWT令牌返回给前端。前端后续请求都在HTTP Header中携带此令牌。后端通过一个自定义的
JwtAuthenticationFilter来解析和验证令牌,并将用户信息放入SecurityContext。这样设计的好处是服务端无需存储会话状态,更易于扩展。 - API文档:SpringDoc OpenAPI 3 (Swagger UI):在
pom.xml中引入springdoc-openapi-ui依赖,简单配置后,访问/swagger-ui.html就能自动生成所有Controller接口的交互式文档。这对于前后端联调、以及日后自己维护代码,价值巨大。 - 其他工具库:
- Lombok:通过注解(
@Data,@Getter,@Setter,@AllArgsConstructor,@NoArgsConstructor)自动生成getter/setter、构造方法等,让实体类和DTO类代码极其简洁。 - MapStruct:用于在不同层之间(如Entity, DTO, VO)进行对象属性拷贝。它会在编译期生成高效的映射代码,性能远优于BeanUtils.copyProperties等反射工具。
- Hutool:一个国产的Java工具类库,提供了字符串处理、日期、加密、IO等众多实用方法,可以避免重复造轮子。
- Lombok:通过注解(
3.2 前端技术考量
虽然标题和热点词聚焦于后端,但一个完整的系统必须考虑前端。对于练手项目,你有两个主流选择:
- 前后端分离(推荐):使用Vue.js(Element UI或Ant Design Vue)或React(Ant Design)构建独立的前端项目。后端通过
@RestController提供纯JSON格式的RESTful API。这是目前绝对的主流开发模式,能让你同时练习后端接口设计和前后端分离协作的流程。 - 服务端渲染:使用Thymeleaf或FreeMarker模板引擎,在Spring Boot后端直接渲染HTML页面。这种方式更传统,开发速度可能更快,但前后端耦合紧密,不利于现代前端技术栈的发挥和团队协作。
对于“源码+数据库+文档”这个目标,我强烈建议采用第一种方案。你可以在项目里建立一个frontend目录存放Vue项目,或者干脆另建一个Git仓库。在文档中说明前后端如何分别启动、如何联调即可。
3.3 项目分层架构(MVC演进版)
一个结构清晰的代码组织方式至关重要。推荐采用以下分层,这是对经典MVC的细化,更符合企业级开发习惯:
com.xxx.rental ├── RentalApplication.java // SpringBoot主启动类 ├── config // 配置类包 │ ├── SecurityConfig.java // Spring Security配置 │ ├── WebMvcConfig.java // 跨域、拦截器等配置 │ └── SwaggerConfig.java // OpenAPI配置 ├── controller // 控制层,接收请求,调用Service,返回结果 │ ├── api // 前后端分离的API接口 │ │ ├── HouseController.java │ │ ├── AuthController.java │ │ └── ... │ └── web // 如果用了Thymeleaf,存放页面控制器 ├── service // 业务逻辑层 │ ├── impl // 服务实现类 │ │ ├── HouseServiceImpl.java │ │ └── ... │ └── HouseService.java // 服务接口 ├── repository // 数据访问层(JPA Repository接口) │ ├── HouseRepository.java │ └── ... ├── model // 实体与数据传输对象 │ ├── entity // JPA实体类,对应数据库表 │ │ ├── User.java │ │ ├── House.java │ │ └── ... │ ├── dto // 数据传输对象,用于接口入参出参 │ │ ├── HouseDTO.java │ │ ├── LoginDTO.java │ │ └── ... │ └── vo // 视图对象,用于返回给前端的特定数据组合 │ └── HouseDetailVO.java ├── util // 工具类 │ ├── JwtUtil.java // JWT工具类 │ └── ... └── exception // 全局异常处理 ├── GlobalExceptionHandler.java └── BusinessException.java // 自定义业务异常为什么这么分?Entity只负责定义数据库表结构;DTO负责在前后端或服务层间传输数据,它可能只是Entity的子集或组合;VO则是为前端页面“量身定制”的展示模型。通过MapStruct在它们之间转换,可以保持各层职责清晰,避免数据库结构直接暴露给前端。
4. 数据库设计与核心表结构解析
数据库设计是系统的基石。设计时要遵循规范化原则,避免数据冗余,同时也要考虑查询性能。以下是核心表结构及其字段说明。
4.1 核心实体关系图(E-R概念)
用户(租客/房东)可以发布/收藏多套房源。一套房源可以被多个用户收藏。一个用户可以发起多个租赁订单,但一个订单只对应一套房源和一个租客。合同与订单一对一关联。
4.2 数据表详细设计
以下SQL以MySQL语法为例,并包含必要的索引和注释。
-- 用户表 (包含租客、房东、管理员,通过user_type字段区分) CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名,唯一', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `user_type` tinyint NOT NULL DEFAULT '0' COMMENT '用户类型:0-租客,1-房东,2-管理员', `id_card` varchar(20) DEFAULT NULL COMMENT '身份证号(房东需实名)', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名(房东需实名)', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_phone` (`phone`), KEY `idx_user_type` (`user_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 房源信息表 CREATE TABLE `house_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '房源标题', `description` text COMMENT '详细描述', `city` varchar(50) NOT NULL COMMENT '城市', `district` varchar(50) NOT NULL COMMENT '区域/区', `address` varchar(200) NOT NULL COMMENT '详细地址', `price` decimal(10,2) NOT NULL COMMENT '月租金,单位元', `area` decimal(6,2) NOT NULL COMMENT '面积,平方米', `room` tinyint NOT NULL COMMENT '卧室数量', `living_room` tinyint DEFAULT '0' COMMENT '客厅数量', `bathroom` tinyint DEFAULT '1' COMMENT '卫生间数量', `floor` smallint DEFAULT NULL COMMENT '所在楼层', `total_floor` smallint DEFAULT NULL COMMENT '总楼层', `orientation` varchar(10) DEFAULT NULL COMMENT '朝向', `landlord_id` bigint NOT NULL COMMENT '房东ID,关联sys_user.id', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-待审核,1-已上架,2-已出租,3-已下架', `view_count` int DEFAULT '0' COMMENT '浏览量', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_landlord` (`landlord_id`), KEY `idx_city_district` (`city`,`district`), KEY `idx_price` (`price`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房源信息表'; -- 房源图片表 (与房源一对多) CREATE TABLE `house_picture` ( `id` bigint NOT NULL AUTO_INCREMENT, `house_id` bigint NOT NULL COMMENT '房源ID', `url` varchar(500) NOT NULL COMMENT '图片存储路径或URL', `is_main` tinyint(1) DEFAULT '0' COMMENT '是否为主图:0-否,1-是', `sort_order` int DEFAULT '0' COMMENT '排序', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_house_id` (`house_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房源图片表'; -- 收藏表 (用户与房源多对多关系) CREATE TABLE `favorite` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `house_id` bigint NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_house` (`user_id`,`house_id`), -- 防止重复收藏 KEY `idx_user_id` (`user_id`), KEY `idx_house_id` (`house_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表'; -- 租赁订单表 CREATE TABLE `rent_order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单号', `order_no` varchar(32) NOT NULL COMMENT '订单流水号(可自定义规则生成)', `house_id` bigint NOT NULL, `tenant_id` bigint NOT NULL COMMENT '租客ID', `landlord_id` bigint NOT NULL COMMENT '房东ID', `monthly_rent` decimal(10,2) NOT NULL COMMENT '月租金', `deposit` decimal(10,2) NOT NULL COMMENT '押金(通常为x个月租金)', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额(首期租金+押金)', `lease_start` date NOT NULL COMMENT '租约开始日期', `lease_end` date NOT NULL COMMENT '租约结束日期', `order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-待支付,1-已支付/待签约,2-已生效,3-已完成,4-已取消', `payment_status` tinyint DEFAULT '0' COMMENT '支付状态:0-未支付,1-已支付', `cancel_reason` varchar(200) DEFAULT NULL COMMENT '取消原因', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_tenant_id` (`tenant_id`), KEY `idx_landlord_id` (`landlord_id`), KEY `idx_house_id` (`house_id`), KEY `idx_order_status` (`order_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租赁订单表'; -- 合同表(与订单一对一或一对多,此处简化为一对一) CREATE TABLE `contract` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL UNIQUE COMMENT '关联订单ID', `contract_no` varchar(50) NOT NULL COMMENT '合同编号', `content` text COMMENT '合同正文(HTML或富文本格式)', `tenant_signature` varchar(500) DEFAULT NULL COMMENT '租客电子签名/签章图', `landlord_signature` varchar(500) DEFAULT NULL COMMENT '房东电子签名/签章图', `signed_time` datetime DEFAULT NULL COMMENT '双方签署完成时间', `pdf_url` varchar(500) DEFAULT NULL COMMENT '生成的PDF合同存储地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同表';设计要点解析:
- 字段注释:每个字段都加了
COMMENT,这对于后期维护和团队协作至关重要。 - 索引策略:除了主键,在经常用于查询条件的字段上建立了索引,如
username(唯一)、phone、city/district、price、status、外键字段等。但索引不是越多越好,它会降低写操作速度。 - 金额字段:使用
DECIMAL(10,2)类型,精确存储小数,避免浮点数精度问题。 - 状态字段:使用
TINYINT表示各种状态,并在代码中用枚举类对应,提高可读性。 - 时间字段:
create_time和update_time是标配,MySQL可以自动更新update_time。
5. 核心功能模块实现详解
有了清晰的设计,接下来就是编码实现。我们挑几个最具代表性的功能模块,看看如何用SpringBoot落地。
5.1 用户认证与JWT集成
这是系统的安全大门。我们采用Spring Security + JWT的方案。
第一步:引入依赖在pom.xml中添加Spring Security和JWT工具库(如jjwt)的依赖。
第二步:创建JWT工具类JwtUtil.java负责生成、解析和验证JWT令牌。
@Component public class JwtUtil { @Value("${jwt.secret}") // 从application.yml读取密钥 private String secret; @Value("${jwt.expiration}") private Long expiration; public String generateToken(String username, List<String> roles) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() .setSubject(username) .claim("roles", roles) // 将角色信息存入claim .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // ... 解析token、获取用户名、验证token等方法 }第三步:实现JWT认证过滤器创建一个JwtAuthenticationFilter继承OncePerRequestFilter。在doFilterInternal方法中,从请求头Authorization中提取JWT令牌,调用JwtUtil解析,如果有效,则构造一个UsernamePasswordAuthenticationToken对象并设置到SecurityContextHolder中,这样后续的接口就能通过@AuthenticationPrincipal获取当前用户信息了。
第四步:配置Spring SecuritySecurityConfig.java是关键。这里需要:
- 注入自定义的
JwtAuthenticationFilter。 - 配置
HttpSecurity,放行登录、注册等公开接口的路径。 - 配置密码编码器为
BCryptPasswordEncoder。 - 配置异常处理(如返回统一的JSON格式,而不是跳转登录页)。
第五步:实现登录接口在AuthController中,接收用户名密码,调用UserDetailsService验证,验证通过后调用JwtUtil.generateToken()生成令牌返回给前端。
实操心得:JWT令牌一旦签发,在有效期内无法作废。这是JWT的一个特点。如果需要在用户登出或修改密码后立即令旧令牌失效,需要一个额外的方案,例如维护一个短小的“令牌黑名单”缓存(Redis),或者在令牌中存入一个版本号(version),用户关键信息变更时更新版本号,校验令牌时对比版本号。对于这个练手项目,可以只依赖较短的令牌过期时间(如2小时)来平衡安全与复杂度。
5.2 房源信息管理(CRUD与复杂查询)
这是业务的核心。我们以房源的分页条件查询和发布为例。
实体类House.java:
@Entity @Table(name = "house_info") @Data // Lombok注解,生成getter/setter等 public class House { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String description; private String city; private String district; // ... 其他字段 @ManyToOne(fetch = FetchType.LAZY) // 多套房源属于一个房东 @JoinColumn(name = "landlord_id") private User landlord; @OneToMany(mappedBy = "house", cascade = CascadeType.ALL, orphanRemoval = true) private List<HousePicture> pictures = new ArrayList<>(); @Enumerated(EnumType.ORDINAL) private HouseStatus status; // ... createTime, updateTime }Repository接口HouseRepository.java:
@Repository public interface HouseRepository extends JpaRepository<House, Long>, JpaSpecificationExecutor<House> { // JpaSpecificationExecutor 用于支持动态条件查询 }Service层实现复杂查询: 我们使用Specification来构建动态查询条件。
@Service @RequiredArgsConstructor // Lombok注解,生成构造器注入 public class HouseServiceImpl implements HouseService { private final HouseRepository houseRepository; private final HouseConverter houseConverter; // MapStruct Mapper @Override public Page<HouseVO> searchHouses(HouseQueryDTO queryDTO, Pageable pageable) { Specification<House> spec = (root, query, cb) -> { List<Predicate> predicates = new ArrayList<>(); if (StringUtils.hasText(queryDTO.getCity())) { predicates.add(cb.equal(root.get("city"), queryDTO.getCity())); } if (StringUtils.hasText(queryDTO.getDistrict())) { predicates.add(cb.equal(root.get("district"), queryDTO.getDistrict())); } if (queryDTO.getMinPrice() != null) { predicates.add(cb.ge(root.get("price"), queryDTO.getMinPrice())); } if (queryDTO.getMaxPrice() != null) { predicates.add(cb.le(root.get("price"), queryDTO.getMaxPrice())); } // 只查询已上架的房源 predicates.add(cb.equal(root.get("status"), HouseStatus.PUBLISHED)); return cb.and(predicates.toArray(new Predicate[0])); }; Page<House> housePage = houseRepository.findAll(spec, pageable); // 使用MapStruct将Entity Page转换为VO Page return housePage.map(houseConverter::toVO); } }Controller层:
@RestController @RequestMapping("/api/houses") @RequiredArgsConstructor public class HouseController { private final HouseService houseService; @GetMapping("/search") public Result<Page<HouseVO>> searchHouses(@Valid HouseQueryDTO queryDTO, @PageableDefault(size = 10, sort = "createTime", direction = Sort.Direction.DESC) Pageable pageable) { Page<HouseVO> page = houseService.searchHouses(queryDTO, pageable); return Result.success(page); } @PostMapping @PreAuthorize("hasRole('LANDLORD')") // 只有房东角色可以发布 public Result<Void> publishHouse(@Valid @RequestBody HousePublishDTO publishDTO, @AuthenticationPrincipal UserDetails userDetails) { Long landlordId = Long.parseLong(userDetails.getUsername()); // 从token中获取用户ID houseService.publishHouse(publishDTO, landlordId); return Result.success(); } }5.3 租赁订单与状态流转
订单是交易的核心,其状态管理是业务逻辑最复杂的地方之一。
状态枚举:
public enum OrderStatus { PENDING_PAYMENT(0, "待支付"), PAID(1, "已支付"), EFFECTIVE(2, "已生效"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); // ... 构造方法,getter }创建订单服务方法:
@Transactional(rollbackFor = Exception.class) // 开启事务 public String createOrder(OrderCreateDTO createDTO, Long tenantId) { // 1. 校验房源是否存在且可租 House house = houseRepository.findById(createDTO.getHouseId()) .orElseThrow(() -> new BusinessException("房源不存在")); if (house.getStatus() != HouseStatus.PUBLISHED) { throw new BusinessException("房源当前不可租"); } // 2. 校验租约日期是否冲突(简化:假设一个房源同一时间只能有一个有效订单) boolean conflict = orderRepository.existsByHouseIdAndStatusInAndLeasePeriodOverlap(...); if (conflict) { throw new BusinessException("该时间段房源已被预订"); } // 3. 生成唯一订单号(雪花算法或时间戳+随机数) String orderNo = generateOrderNo(); // 4. 构建订单实体并保存 RentOrder order = new RentOrder(); order.setOrderNo(orderNo); order.setHouse(house); order.setTenantId(tenantId); order.setLandlordId(house.getLandlord().getId()); order.setMonthlyRent(house.getPrice()); order.setDeposit(house.getPrice().multiply(new BigDecimal(2))); // 押二付一 order.setTotalAmount(order.getMonthlyRent().add(order.getDeposit())); order.setLeaseStart(createDTO.getLeaseStart()); order.setLeaseEnd(createDTO.getLeaseEnd()); order.setOrderStatus(OrderStatus.PENDING_PAYMENT); orderRepository.save(order); // 5. 锁定房源状态(可选:改为“已预订”) house.setStatus(HouseStatus.RESERVED); houseRepository.save(house); // 6. 发送通知(异步,可用@Async或消息队列) notificationService.sendNewOrderNotification(order); return orderNo; }支付回调与状态更新: 当用户支付成功,第三方支付平台会异步通知我们的回调接口。在这个接口里,我们需要:
- 验证回调签名,防止伪造请求。
- 根据回调中的商户订单号(即我们的
orderNo)查找订单。 - 校验订单状态是否为
PENDING_PAYMENT。 - 更新订单状态为
PAID,支付状态为已支付。 - 这里又是一个关键事务:更新订单状态的同时,可能还需要生成合同草稿、发送签约提醒等。务必保证这些操作在同一个事务中,要么全成功,要么全失败。
6. 开发中常见问题与实战排查技巧
在实际编码和调试过程中,你几乎一定会遇到下面这些问题。我把它们和解决思路记录下来,希望能帮你少走弯路。
6.1 数据库连接与JPA映射问题
问题:启动时报
Table 'xxx' doesn't exist或字段映射错误。排查:
- 检查
application.yml中的数据库连接URL、用户名密码是否正确。 - 检查实体类
@Table和@Column注解的名称是否与数据库表/字段一致。默认情况下,JPA会将驼峰属性名转换为下划线列名(如userName->user_name)。 - 如果数据库表是已有的,设置
spring.jpa.hibernate.ddl-auto=validate,让Hibernate验证映射是否匹配,而不是自动创建表。 - 使用
spring.jpa.show-sql=true在控制台打印生成的SQL,这是排查映射问题最直接的利器。
- 检查
问题:进行关联查询(如查询房源连带房东信息)时,出现
LazyInitializationException。排查:这是经典的“懒加载异常”。发生在你从数据库取出实体(
House)后,Session已关闭,但又在Controller或视图层尝试访问其懒加载属性(如house.getLandlord().getNickName())。解决:
- 方法一(推荐):在Service层查询时,就通过
@EntityGraph注解或JOIN FETCH语句主动抓取需要的数据。
@EntityGraph(attributePaths = {"landlord"}) Optional<House> findWithLandlordById(Long id);- 方法二:使用DTO/VO模式,在Service层或专门的Converter中,将Entity转换为VO时,手动组装所需数据,避免直接返回Entity。
- 方法一(推荐):在Service层查询时,就通过
6.2 事务管理不生效
- 问题:方法上加了
@Transactional,但抛出异常后数据还是被保存了。 - 排查:
- 确保异常类型是
RuntimeException或Error。默认只回滚这些异常。如果是受检异常(如IOException),需要指定@Transactional(rollbackFor = Exception.class)。 - 确保方法是被Spring代理对象调用的。在同一个类中,一个非事务方法A调用同一个类中的事务方法B,B的事务是不会生效的(因为绕过了代理)。这是Spring AOP的特性。
- 检查数据库引擎是否支持事务(如InnoDB支持,MyISAM不支持)。
- 确保异常类型是
6.3 文件上传与存储路径
- 问题:上传的图片,在项目重启后访问不到了。
- 排查:如果你把图片保存在项目内部的
static/upload目录下,当打成jar包运行或重启后,这个路径可能不可写或丢失。 - 解决:
- 方案一(开发方便):在
application.yml中配置一个绝对路径。
然后在代码中file: upload-dir: /opt/rental/uploads/new File(uploadDir, fileName)进行保存。- 方案二(生产推荐):集成云存储OSS(阿里云、腾讯云COS等)。文件上传后直接拿到一个永久的URL,彻底解决存储和访问问题。对于练手项目,可以先用方案一,但在文档中说明生产环境的推荐方案。
- 方案一(开发方便):在
6.4 日期时间处理
- 问题:前端传过来的日期字符串(如
"2023-10-01")无法反序列化为LocalDate或Date。 - 解决:
- 在DTO的字段上使用
@JsonFormat注解指定格式。
@JsonFormat(pattern = "yyyy-MM-dd") private LocalDate leaseStart;- 或者,配置一个全局的
Jackson序列化/反序列化规则。
- 在DTO的字段上使用
6.5 跨域问题(CORS)
- 问题:前端运行在
localhost:8080,后端在localhost:8081,前端调用接口时浏览器报CORS错误。 - 解决:在后端添加一个全局的CORS配置。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 针对所有/api开头的接口 .allowedOrigins("http://localhost:8080") // 允许的前端地址 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true); // 如果前端请求带cookie/token,需要为true } }7. 项目部署与后期优化方向
让项目在本地跑起来只是第一步,如何让它成为一个“像样”的、可交付的项目,还需要最后几步。
7.1 基础部署方案
- 打包:使用
mvn clean package生成可执行的jar文件(target/*.jar)。 - 环境配置:通过
application-prod.yml文件配置生产环境的数据库连接、日志级别、文件上传路径等。使用--spring.profiles.active=prod启动参数激活。 - 数据库准备:将
src/main/resources下的schema.sql(建表)和data.sql(初始数据)在MySQL中执行,或者使用Flyway/Liquibase进行数据库版本管理。 - 运行:在服务器上使用
nohup java -jar rental-application.jar --spring.profiles.active=prod > app.log 2>&1 &命令后台启动。 - 前端部署:将Vue项目
npm run build后生成的dist目录内容,放到Nginx或Apache的静态资源目录下,并配置反向代理,将/api请求转发到后端SpringBoot服务。
7.2 从“能跑”到“好用”的优化点
如果你想让这个项目在面试或作品集中更出彩,可以考虑以下优化方向:
- 加入缓存:房源列表、热门房源等信息,变化不频繁但查询量大,非常适合用Redis做缓存。在Service层,先查缓存,命中则返回,未命中则查数据库并写入缓存。
- 引入消息队列:将发送邮件、短信通知等非核心、耗时的操作异步化。用户下单后,只需将“发送通知”这个消息丢进RabbitMQ或Kafka,然后立即返回响应。由单独的消息消费者去处理发送逻辑,提升系统响应速度和解耦。
- 接口限流与降级:使用Spring Cloud Alibaba Sentinel或Resilience4j,对核心接口(如查询、下单)做限流,防止恶意刷单或突发流量打垮服务。
- 分库分表(远期):如果房源和订单数据量达到百万、千万级,单表查询性能会下降。可以考虑按城市对房源表进行分库分表,这是一个经典的面试话题。
- 完善监控与日志:使用Spring Boot Actuator暴露健康检查、指标等信息。集成ELK(Elasticsearch, Logstash, Kibana)或Prometheus + Grafana搭建日志和监控平台。
完成这个项目的过程,远比最终的那份源码和文档重要。你会经历需求分析、技术选型、数据库设计、编码实现、调试排错、部署上线的完整流程。每一个踩过的坑,每一个解决问题的夜晚,都会转化为你简历上实实在在的“项目经验”和面试时自信的谈资。动手去做,从第一个@SpringBootApplication注解开始。
本文还有配套的精品资源,点击获取