
简介基于SpringBoot的大学生租房管理系统源码专为Java毕设与租房平台学习者设计实现房东发房、学生找房、在线预约等核心流程。技术栈覆盖SpringBoot、MyBatisPlus、Vue、MySQL并含Eclipse/Idea等多工具部署说明。压缩包共865个文件、18.52MB主要包含139个Java后端代码、51个Vue组件、164个JS逻辑、53个CSS样式及SQL脚本、XML配置、PDF文档等目录结构完整开发环境与数据库设计均可直接参考。项目提供install/run/build批量脚本便于快速启动调试同时附有系统分析、技术选型及源码注释适合用于课程设计、毕业设计或SpringBoot实战入门。目前已有188人学习下载对想要获取完整前后端联调案例的读者参考价值明确。1. 大学生租房系统与 Spring Boot为什么毕设和上线都用它每年毕业季前后大学生租房需求集中爆发房源信息混乱、看房预约靠微信接龙、合同签署后纠纷频发。一个面向大学生场景的租房管理系统核心不只是发布房源而要解决“学生身份可信度”“看房排期冲突”“租金支付节点”这三个实际问题。基于 Spring Boot 来落地这类系统几乎是当前最稳妥的选择它自带内嵌容器、自动配置和丰富的 Starter能让一个 Java 后端项目在 10 分钟内跑起来同时保持清晰的 MVC 分层给后继维护留出余地。本文会顺着一条完整可复现的路径从数据库表设计讲到 JWT 权限控制再到部署和并发兜底适合正在做毕设、想要源码风格参考、或者接手二手项目时需要快速看懂的工程师。2. 基于 Spring Boot 的大学生租房系统技术选型与数据库设计2.1 选型Spring Boot MyBatis-Plus MySQL 的理由在进入编码前先把技术栈定下来。大学生租房系统的典型特征是实体数量少、但查询组合多比如“近地铁、可月付、有独立卫浴、女生优先”。如果只靠 Spring Data JPA虽然上手快但复杂条件查询时不太好拼动态 SQL原生 MyBatis 又需要写大量 XML。常见的折中方案是 MyBatis-Plus它在 Spring Boot 生态里与 MyBatis 完全兼容提供内置的 CRUD 方法和分页插件条件构造器可以直接撑起多条件检索。数据库方面MySQL 8.x 是标配存储引擎选择 InnoDB。需要注意一个小坑如果所在省份的高校毕业时间集中在 6 月那么房源和订单表的写入峰值也会出现在同一阶段所以表设计时务必要区分开热数据与全量数据。不要把浏览记录或日志表放在业务库中膨胀否则后续 Slow Query 会很难收拾。Redis 在这一版建议只做两件事缓存热门房源列表、存储验证码。不把订单状态推进事务依赖于 Redis原因在于学生端看房预约可能同时并发预约同一个时段一旦 Redis 和 MySQL 状态不一致后续的纠纷处理成本很高。前面的技术选型里如果只用一个关系型数据库反而更容易保证业务闭环。2.2 核心数据表设计用户、房源、订单、收藏我以为可以先用一张表把用户、房东、管理员都装下但实践下来还是不要偷懒。学生和房东在字段要求、审核流程上差异很大。用user_type区分可行但“学生实名认证”需要学院和学号字段房东需要身份证号和房产证明混在一起会让表膨胀。我会拆成user主表加student_info和landlord_info两个扩展表。订单表则需要做到只增不改对比字段说明关键约束order_no订单编号用于展示和售后唯一索引house_id房源 ID普通索引tenant_id租客用户 ID普通索引owner_id房东用户 ID普通索引status0待支付 1已支付 2已完成 3已取消与 status_time 配合status_time最近一次状态变更时间查询常用visit_time看房预约时间需要防冲突见下房源表设计时应该把价格拆成rent_price和deposit不要把押一付三写在一个字符串里否则后面做筛选排序时会变成全表扫描。下面这一段 SQL 是按可运行标准写的CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL COMMENT 房源标题, address_detail VARCHAR(255) NOT NULL, metro_line VARCHAR(50) DEFAULT NULL COMMENT 地铁线如5号线, rent_price DECIMAL(10,2) NOT NULL, deposit DECIMAL(10,2) NOT NULL DEFAULT 0, house_type TINYINT NOT NULL DEFAULT 1 COMMENT 1合租 2整租, gender_limit TINYINT NOT NULL DEFAULT 0 COMMENT 0不限 1仅男 2仅女, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1上架 2已下架, tenant_facing TINYINT NOT NULL DEFAULT 1 COMMENT 是否面向学生, cover_url VARCHAR(255) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status_price (status, rent_price), KEY idx_metro (metro_line) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表;说明一下idx_status_price是个联合索引覆盖“按状态过滤后按价格排序”这个高频查询。metro_line单独建索引是因为大学生租房对地铁依赖度非常高而且它的区分度足够。不要把address_detail加索引长字符串索引会浪费空间且对查询提升有限。再来是订单状态机。用status字面量加status_time的方式比维护一张状态流转表更轻量。但要保证同一订单的并发操作不能互相覆盖所以update语句必须带状态条件UPDATE house_order SET status 1, status_time NOW() WHERE order_no ? AND status 0;这种乐观更新能避免用户连续点击两次“立即预约”产生两条待支付订单。如果数据库影响行数为 0说明状态已变或订单不存在业务层就要给出明确提示而不是无脑刷新。2.2.1 看房预约时间如何防冲突预约冲突是租房系统里很容易翻车的地方。最直接的做法是给house_id和visit_time加唯一索引但一个房源可能会有多个可用时间段这样做会把“每天 9 点到 18 点多个时段”变成死结构。我的做法是建一张house_visit_slot表把每个房源按小时拆成可预约位CREATE TABLE house_visit_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL, visit_date DATE NOT NULL, visit_time VARCHAR(10) NOT NULL COMMENT 如 10:00, reserved TINYINT NOT NULL DEFAULT 0, user_id BIGINT DEFAULT NULL, UNIQUE KEY uk_house_slot (house_id, visit_date, visit_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;预约时执行INSERT ... ON DUPLICATE KEY UPDATE user_idVALUES(user_id), reserved1如果插入影响行数为 1说明抢占成功为 0 则说明该时段已经被预约。这个方案的好处是不需要额外引入分布式锁把并发控制交给数据库唯一索引在单库场景下既简单又可靠。3. 从零搭建租房系统后端Spring Boot 项目结构与核心代码3.1 项目骨架与分层一个可维护的 Spring Boot 项目我习惯把它切成分层清晰的 Maven 模块但如果是单模块也至少要保持下面的包结构com.example.rental ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── config ├── commonentity层只做数据库映射字段与表一一对应dto层面向接口入参与出参避免把实体直接暴露给前端。常见错误是直接在controller里用Map接收参数一旦参数名改动编译器不会帮你找出来重构成本极高。对于毕设项目这个分层能保证答辩时讲得清楚也方便后续加入缓存和消息队列。Maven 的pom.xml里需要引入的核心依赖是这些dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencySpring Boot 3.x 与 MyBatis-Plus 3.5.5 的兼容性是可以直接用的但如果你还在用 Spring Boot 2.7那么 MyBatis-Plus 请选择 3.5.3.x 这个区间。版本不匹配时最常见的报错是nested exception is java.lang.NoClassDefFoundError搜索时容易被误导实际上就是依赖冲突。application.yml 里面需要重点关注的是 MyBatis-Plus 的配置项spring: datasource: url: jdbc:mysql://localhost:3306/rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0logic-delete-field是逻辑删除的全局配置这样删除房源时执行的是update语句而不是物理删除。对租房系统而言房源删除后还要保留历史订单数据逻辑删除是必须的。mapper-locations指到mapper目录虽然 MyBatis-Plus 的 BaseMapper 能解决单表操作但多表关联依然需要写 XML这个配置不能省。3.2 用 Spring Boot 实现房源检索接口3.2.1 实体类与 Mapper实体类可以直接用 MyBatis-Plus 的注解这样不去手写resultMap但要注意TableField的existfalse只用于非表字段不要把查询出的关联字段误标成existfalse否则结果集会丢失字段。下面是一个精简后的房源实体Data TableName(house) public class House { TableId(type IdType.AUTO) private Long id; private String title; private BigDecimal rentPrice; private Integer houseType; private Integer status; TableField(exist false) private String ownerName; }Mapper 接口继承 BaseMapper 后单表 CRUD 基本不用写 SQLpublic interface HouseMapper extends BaseMapperHouse { IPageHouse selectPageWithOwner(PageHouse page, Param(req) HouseQueryReq req); }这个selectPageWithOwner需要多表查询所以浩繁写 XML。XML 文件放在resources/mapper/HouseMapper.xml下select idselectPageWithOwner resultTypecom.example.rental.entity.House SELECT h.*, u.nickname AS owner_name FROM house h LEFT JOIN user u ON h.owner_id u.id where if testreq.metroLine ! null and req.metroLine ! AND h.metro_line #{req.metroLine} /if if testreq.maxPrice ! null AND h.rent_price lt; #{req.maxPrice} /if if testreq.genderLimit ! null AND h.gender_limit #{req.genderLimit} /if AND h.status 1 AND h.deleted 0 /where ORDER BY h.rent_price ASC /select3.2.2 Service 层实现分页与缓存Service 层直接返回IPage给 Controller不需要手工处理分页数据。MyBatis-Plus 的分页插件需要配置一个 Bean忘记配置时Page对象不会生效而是查出全量数据这是新手最常见的问题Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页查询写完后可以在service层加一层本地缓存。但我不建议直接用Cacheable无脑缓存因为房源价格、下架状态一变缓存可能继续输出脏数据。更好用的做法是在更新房源状态时拿到被更新的houseId手动把缓存里对应的 key 删除。基于 Spring Boot 的CacheManager来做CacheEvict(value hotHouse, key #houseId) public void updateHouseStatus(Long houseId, Integer status) { House house new House(); house.setId(houseId); house.setStatus(status); houseMapper.updateById(house); }热门房源列表的 key 应该包含“学校区域”和“价格区间”不要只缓存一个热门列表页。同一个学校的宿舍区附近每隔几百米价格就差很多所以key适合这样拼接“hot:schoolId:maxPrice”。3.3 实现看房预约与订单流程预约流程可以用一个事务方法串起来。第一步查house_visit_slot表抢时段第二步创建订单记录第三步更新房源“被预约次数”这个冗余字段。这里要注意事务边界不要把外部接口调用放进同一个事务里否则网络抖动会拖长数据库连接的持有时间。Transactional(rollbackFor Exception.class) public RentOrder createOrder(CreateOrderReq req) { // 1. 抢占看房时段影响行数为 0 则失败 int updated houseVisitSlotMapper.tryReserve(req.getSlotId(), req.getUserId()); // 2. 创建订单 RentOrder order new RentOrder(); order.setOrderNo(generateOrderNo()); order.setHouseId(req.getHouseId()); order.setTenantId(req.getUserId()); order.setStatus(0); orderMapper.insert(order); // 3. 更新冗余字段 houseMapper.increaseLookCount(req.getHouseId()); return order; }tryReserve对应的 SQL 就是前面说过的UPDATE ... WHERE reserved 0它在同一行记录上做条件更新天然避免了重复预约。generateOrderNo()不建议用UUID订单号要短且可读我一般用时间戳加随机数再加两位校验位public String generateOrderNo() { String yyyyMMddHHmmss DateTimeFormatter.ofPattern(yyyyMMddHHmmss).format(LocalDateTime.now()); int random ThreadLocalRandom.current().nextInt(1000, 9999); return R yyyyMMddHHmmss random; }如果出现事务回滚但tryReserve已经执行的情况数据库事务会自动回滚掉。但要注意update语句不会因为后面报错而自动回退到原值必须依赖 Spring 事务管理。因此在 Controller 里不要再 try-catch 吞掉异常否则事务拦截器就看不到异常无法回滚。4. 租客端与房东端的权限控制Spring Security JWT 落地4.1 为什么用 JWT 而不用 Session大学生租房系统前后端分离时最常见的是 Spring Boot 提供 JSON API前端用 Vue 或小程序调用。如果还用 Session就需要处理跨域 Cookie 的SameSite问题并且服务端要保持会话状态对多实例部署不友好。JWT 的无状态特性适合这个场景用户登录后拿 token每个请求带在Authorization头里服务端不保存登录态天然适配容器化部署。但 JWT 的缺点是 token 无法在服务端主动失效。如果学生把手机弄丢了token 要过期才能登出。所以这里要配置一个比较合理的过期时间我一般设成 2 小时并配合一个 redis 黑名单。用户点击退出时把 token 的 jti 写入黑名单过期时间与 token 一致。4.2 基于注解的接口鉴权Spring Security 集成 JWT 后需要定义两个核心组件过滤器JwtAuthenticationFilter和SecurityConfig。过滤器负责解析请求头如果 token 合法就把用户信息和角色塞进SecurityContext。然后在需要权限的接口上使用PreAuthorize注解这是最直观的写法RestController RequestMapping(/api/order) public class OrderController { PostMapping(/create) PreAuthorize(hasRole(STUDENT)) public R createOrder(RequestBody CreateOrderReq req) { return R.ok(orderService.createOrder(req)); } PutMapping(/finish/{orderId}) PreAuthorize(hasRole(LANDLORD) or hasRole(ADMIN)) public R finishOrder(PathVariable Long orderId) { return R.ok(orderService.finishOrder(orderId)); } }hasRole(STUDENT)生效的前提是JwtAuthenticationFilter里把角色放进SimpleGrantedAuthority时要带上前缀ROLE_否则源码里会一直提示Access is denied而且日志里也不会打印具体原因。例如new SimpleGrantedAuthority(ROLE_ user.getUserType())。过期或者签名不匹配时不要直接抛 500应该返回一个401状态码和固定格式的 JSON 错误体前端才有可能统一拦截。4.3 密码加密与角色区分密码存储不能再用MD5Spring Security 自带的BCryptPasswordEncoder是最稳妥的。它每次加密结果都不一样还支持校验所以不需要自己做加盐Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } public String encodePwd(String rawPwd) { return passwordEncoder.encode(rawPwd); }要注意一个问题如果学生用户导入历史数据时密码字段是 MD5 密文那么直接用BCryptPasswordEncoder.matches()是校验不过的。常见做法是在User实体里增加pwd_version字段0表示 MD5、1表示 BCrypt。登录时先判断版本如果是老用户就先转成 BCrypt 再更新密码。这在归档数据导入时非常有用。下表是角色与接口访问的对应关系项目里可以照这张表去配PreAuthorize角色可访问核心接口典型业务STUDENT检索房源、创建预约、发起合同浏览与预租LANDLORD房源管理、订单确认、收款上架与确认ADMIN审核房源、管理用户、品牌通告平台运营5. 毕设答辩与上线部署3 个能加分的细节5.1 用 Docker Compose 一键启动 MySQL 和 Redis如果答辩时演示环境经常因为 MySQL 没启动而尴尬用 Docker Compose 是最省心的。在项目根目录加一个docker-compose.yml一条命令拉起依赖services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: rental ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:7-alpine ports: - 6379:6379然后执行docker compose up -d再启动 Spring Boot 本地应用。注意 MySQL 8 的授权插件是caching_sha2_password如果连接时报 Public Key Retrieval 错误就在 JDBC 连接串上加上allowPublicKeyRetrievaltrueuseSSLfalse。5.2 慢查询日志与索引优化验证除了功能跑通答辩时能说出“我优化过索引”会明显加分。MySQL 开启慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;然后在本地压测接口把实际查询 SQL 拿出来EXPLAIN检查type和key字段。最应该盯着的指标是type从ALL变成ref或者range而不是只看结果是否正常。例如搜索房源时如果只给status建索引而查询条件是status 1 AND rent_price 3000联合索引idx_status_price会好很多因为避免回表多次读数据。5.3 并发场景下的库存扣减乐观锁解决抢房热门房源在开学季可能同时被十几个人抢先预约这种情况下用数据库更新加版本号比用分布式锁简单得多。给house表加一列version更新时带上版本号UPDATE house SET look_count look_count 1, version version 1 WHERE id ? AND version ?;如果影响行数为 0则说明其他线程已经抢先更新业务层可以返回“房源预约人数已满”。这种逻辑写进houseMapper里不要先在 Java 里读出look_count再计算否则会出现丢失更新。MyBatis-Plus 也支持Version注解Version private Integer version;于是updateById会自动带上版本条件不需要手写 SQL 片段。这个细节面试官问你“超卖怎么处理”时可以直接回答比起空谈 synchronized 要真实很多。本文还有配套的精品资源点击获取