手头这套 Java 同城租房系统源码是我在两轮课程设计基础上重构出来的完整项目实现。最初只是想着把房东发布房源、租客在线找房这条主链路跑通后来越做越发现真正的难点不是增删改查而是“同城”这两个字怎么落进设计里。如果你正在找 Java 项目源码参考又不想只交一个学生管理系统级别的 Demo那这篇文章应该能给你一条可以直接照抄的技术路线。我会从业务边界、技术选型、数据库设计、核心代码实现、后台权限、上线部署六个层面展开讲清楚每个模块为什么要这么做以及我实际开发中踩过的坑。内容比较长建议收藏后对着源码结构慢慢看。1. 同城租房系统的业务边界我把它拆成了五个子域先别急着写代码。很多半途而废的课程设计项目问题都出在业务范围没定清楚做着做着就从租房系统变成了“用户管理系统”。所以我在动手前把系统拆成了五个子域每个子域只解决一件事。1.1 房源域系统的核心资产房源域是最重的一块包含房源发布、编辑、上下架、审核、图片管理、标签管理。房东创建房源时需要填写小区名称、户型、面积、租金、租赁方式、地址描述、配套设施等信息。房源发布后不能直接展示必须经过管理员审核这是为了让系统具备基本的平台监管能力。同城租房和普通租房不一样的地方在于房源必须绑定城市编码和区域编码检索时也一定要带上这个维度。后续要支撑“XX市XX区租房”这种搜索就不能只靠一个地址字符串做模糊匹配。1.2 用户与认证域区分房东、租客和管理员用户域不是简单的注册登录。我设计了三类角色租客、房东、管理员。一个用户可以同时是租客和房东所以用户表单独维护角色字段房源表通过 landlord_id 关联用户而不是把用户类型写死在账号里。认证方式用的是手机号验证码登录配合 JWT 做无状态授权。考虑到实际部署环境我还加了账号密码登录作为兜底方便本地调试和后面接短信服务商之前先用模拟验证码。1.3 订单与预约域把看房和签约流程分开租房业务不是直接线上支付就能完成的线下实地看房是最关键的环节。所以我把流程拆成“预约看房”和“租赁订单”两部分。租客对房源感兴趣后先提交预约选择期望看房时间房东在后台确认或调整时间。看房满意后再生成正式租赁订单。很多参考项目只做“收藏房源”和“在线下单”完全没有预约这个中间态这会导致后续很难扩展在线合同、押金支付这些功能。我认为预约看房是这类系统的业务灵魂必须单独建模。1.4 检索与推荐域同城搜索才是核心竞争力租房系统的检索门槛不高但要做好搜索体验很难。我的做法是城市编码精确过滤 区域编码筛选 价格区间分桶 户型标签多选 排序方式切换。为了后续能展示“附近房源”数据库里还预留了经纬度字段并用空间索引做了基础封装。1.5 运营管理域后台不是附属品完整的项目实现必须包含后台管理系统。管理员可以在后台审核房源、管理用户、发布公告、查看预约记录、统计每日新增房源和注册用户。这部分往往占了总代码量的三分之一却最容易被课程设计忽略。我把这五个子域理清之后才开始画页面和建表。事实证明多花两天梳理业务边界后面写代码的速度反而快了。2. 技术选型与源码工程结构为什么选 Spring Boot MyBatis-Plus很多刚入门的同学还在纠结用 SSH 还是 SSM我的建议是直接选 Spring Boot 2.7.x。理由很现实第三方组件整合成本低starter 机制省掉了大量 XML 配置社区资料最丰富遇到问题基本都能搜到答案。2.1 技术栈清单与选型理由组件版本选型理由JDK1.8兼容性最好几乎所有服务器都能跑。想用新语法也可以上 JDK 17但没必要冒险Spring Boot2.7.x最后一个默认支持 javax.servlet 的版本老项目迁移更稳MyBatis-Plus3.5.x内置分页、条件构造器、逻辑删除能减少 50% 的重复 SQLMySQL8.0同时支持关系数据和空间函数做同城检索不额外引搜索引擎Redis5.x验证码缓存、Token 过期处理、热点房源缓存Spring Security5.7.x和 Spring Boot 集成度高支持方法级权限控制JJWT0.11.x生成和解析 JWT 的轻量库这里特别说一下为什么用 MyBatis-Plus 而不是 JPA。租房系统的查询模式非常固定像“按城市查询房源”“按用户查询收藏记录”这些操作直接用条件构造器写起来很快。JPA 虽然面向对象更强但遇到复杂分页和动态条件时生成出来的 SQL 经常不是我们想要的。MyBatis-Plus 更接近普通开发者的思维习惯排查问题也更直观。2.2 Maven 多模块工程结构我采用的是 Maven 多模块结构虽然单模块也能跑但多模块在简历上写出来更有说服力更重要的是能把职责边界强制拆分。modules modulerent-common/module modulerent-admin/module modulerent-api/module modulerent-framework/module modulerent-job/module /modulesrent-common公共常量、统一返回结果、异常枚举、工具类。rent-framework安全配置、Redis 配置、MyBatis-Plus 配置、通用组件。rent-api面向微信小程序和 H5 的用户端接口。rent-admin面向管理员的运营后台接口。rent-job定时任务模块比如定时清理过期预约、刷新房源状态。如果你觉得多模块麻烦也可以合并成单项目通过包名区分 api、admin、framework效果差不多。我这里保留多模块是为了给后续扩展留空间。2.3 包结构设计的参考模板com.example.rent ├── common │ ├── constant │ ├── enums │ ├── exception │ └── result ├── framework │ ├── config │ ├── security │ ├── redis │ └── interceptor ├── modules │ ├── user │ ├── house │ ├── order │ ├── favorite │ └── admin └── RentApplication.java2.4 初始化环境需要注意的细节需要 JDK 1.8、Maven 3.6、MySQL 8.0、Redis 5其中 Redis 在本地开发时如果没装最简单的方式是使用 Docker 启动一个docker run -d --name rent-redis -p 6379:6379 redis:5.0数据库初始化脚本 init.sql 里除了建表语句之外一定要把默认管理员账号预置进去。否则前端登录时找不到管理员排查半天才发现是初始数据没插入。3. 数据库设计把“同城”两个字真正落进数据表数据库设计是整个项目最见功力的地方。租房系统的表数量一般在 15 张左右这里我挑核心的几张展开说一下。3.1 核心表清单与字段职责表名核心字段说明userid, phone, password, nickname, avatar, city_code, roles用户表city_code 用于同城推荐houseid, landlord_id, title, cover_url, price, rent_type, area, bedroom, parlor, city_code, region_code, address, longitude, latitude, status, audit_status, deleted房源表status 表示上下架audit_status 表示审核状态house_imageid, house_id, image_url, sort_order房源图片最多支持 9 张house_featureid, house_id, feature_name配套标签如“近地铁”“可短租”favoriteid, user_id, house_id, create_time收藏关系表appointmentid, user_id, house_id, visit_time, status, remark预约看房表rental_orderid, order_no, user_id, house_id, landlord_id, amount, deposit, status, start_date, end_date租赁订单表house_audit_logid, house_id, admin_id, action, comment, create_time审核日志browse_recordid, user_id, house_id, create_time浏览记录一张比较容易忽略的关联表是 house_feature。很多项目喜欢在 house 表里放一个 varchar 字段用逗号分隔标签查询时 like 匹配。这种设计在小数据量时能用但后续做标签筛选时 SQL 会写得非常痛苦。我建议还是要单独建表这样统计“带阳台房源”的 count 更快索引也更干净。3.2 地理位置与同城检索的实现方案同城搜索的定位方案我试过三种纯 MySQL 模糊查询、MySQL 空间函数、Redis GEO。对课程设计和中小型项目来说第一种和第三种都能用。第 1 种通过 city_code 和 region_code 精确过滤再配合 order by 字段排序。这种方案 SQL 最简单适合城市粒度搜索例如“武汉租房”只需要 where city_code 027。区域粒度搜索就再加 region_code。第 2 种使用 MySQL 的 ST_Distance_Sphere 函数按经纬度距离排序适合“附近房源”场景。前提是 house 表要建 SPATIAL 索引查询时用 MBRContains 先圈出一个候选框再排序否则全表计算会很慢。我的项目里是两种组合使用列表页默认走 city_code region_code详情页附带经纬度预留了一个距离排序接口。不要一开始就上 Elasticsearch对同城房源这种几千几万条的数据量来说属于过度设计徒增部署复杂度。3.3 订单状态机和房源状态联动设计数据库时要考虑状态流转不能放任代码里随意更新状态。我用枚举类 StateEnum 统一管理房源和订单的状态。房源的 status 有三个值0 待上架、1 上架中、2 已下架。audit_status 有三个值0 待审核、1 审核通过、2 审核拒绝。房东上架新房源时audit_status 重置为待审核只有审核通过后 status 才能变为 1。这就是为什么我在字段设计上把审核状态和展示状态分开避免出现“审核通过但展示不下”的混乱。预订流程的状态机更关键待看房(APPOINTMENT_PENDING) - 已确认(APPOINTMENT_CONFIRMED) - 已完成(FINISHED) - 已取消(CANCELED) 确认后生成租赁订单 - 待签约 - 已签约 - 已退租状态之间的转换都在 service 层做校验事务由 Transactional 保证。这里要特别注意幂等性问题用户重复提交预约时如果上一次预约还没被处理就不能再插入一条重复预约。我是在 appointment 表建了唯一索引 uk_user_house_timeuser_id, house_id, visit_time数据库层面直接挡住重复提交。4. 核心功能开发从注册登录到房源下单这一部分我不把完整代码贴出来那太长重点讲几个关键链路的设计思路和代码骨架你拿到源码后能快速定位到对应类。4.1 手机号验证码登录与 JWT 签发登录逻辑分成三步请求发验证码、提交验证码换取 Token、鉴权拦截器校验 Token。发验证码接口的核心是把验证码存到 Redis并设置 5 分钟过期public void sendVerifyCode(String phone) { String code String.valueOf(new Random().nextInt(999999)); redisTemplate.opsForValue() .set(RedisKeyUtil.smsCode(phone), code, 5, TimeUnit.MINUTES); }本地环境我默认开启 mock 模式验证码固定为 123456方便调试。上生产环境前关闭 mock接入真正的短信供应商即可。登录成功后我用 JJWT 生成 Token并把用户 id 放进 claimsString token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(role, user.getRoles()) .setExpiration(new Date(System.currentTimeMillis() 7L * 24 * 3600 * 1000)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact();4.2 房东发布房源的完整链路房东发布房源的接口背后其实是一个事务性操作先插入 house 主表再批量插入 house_image 和 house_feature。我在创建房源时强制要传至少一张封面图没有封面图直接抛异常否则前端列表页会很难看。Transactional public Long createHouse(HouseCreateRequest request) { House house new House(); BeanUtils.copyProperties(request, house); house.setLandlordId(currentUserId()); house.setStatus(HouseStatusEnum.OFF_SHELF.getCode()); house.setAuditStatus(AuditStatusEnum.PENDING.getCode()); houseMapper.insert(house); if (CollectionUtils.isNotEmpty(request.getImageUrls())) { houseImageMapper.batchInsert(house.getId(), request.getImageUrls()); } return house.getId(); }创建成功后需要调用接口提交审核也就是把 audit_status 从待审核改为待审核初始状态其实我是专门放了 submitSubmitAudit 接口让房东可以先保存草稿再提交审核通过后自动上架。4.3 同城房源检索与筛选搜索接口最核心的是 MyBatis-Plus 的条件构造器。我在 Service 层拼 QueryWrapper注意所有的筛选字段都用 city_code、region_code、price_min、price_max、rent_type 这样的参数而不是让前端直接传 SQL 片段。public IPageHouseVO searchHouse(Page page, HouseQuery query) { LambdaQueryWrapperHouse wrapper new LambdaQueryWrapper(); wrapper.eq(House::getStatus, HouseStatusEnum.ON_SHELF.getCode()) .eq(query.getCityCode() ! null, House::getCityCode, query.getCityCode()) .eq(query.getRegionCode() ! null, House::getRegionCode, query.getRegionCode()) .between(query.getMinPrice() ! null query.getMaxPrice() ! null, House::getPrice, query.getMinPrice(), query.getMaxPrice()) .eq(query.getRentType() ! null, House::getRentType, query.getRentType()) .orderByDesc(House::getCreateTime); return houseMapper.selectPage(page, wrapper); }这里有个小坑当 minPrice 为空、maxPrice 不为空时between 条件不会生效因为我在第一个判断里写了两个字段都不为空。实际应该拆成两个 ge 和 le 判断避免逻辑漏洞。4.4 预约看房与订单生成预约接口不复杂重点在状态校验public Long createAppointment(AppointmentRequest request) { House house houseMapper.selectById(request.getHouseId()); if (house null || house.getStatus() ! HouseStatusEnum.ON_SHELF.getCode()) { throw new BizException(ErrorCode.HOUSE_NOT_AVAILABLE); } Appointment appointment new Appointment(); appointment.setUserId(currentUserId()); appointment.setHouseId(request.getHouseId()); appointment.setVisitTime(request.getVisitTime()); appointment.setStatus(AppointmentStatusEnum.PENDING.getCode()); appointmentMapper.insert(appointment); return appointment.getId(); }房东端确认预约后系统自动生成一条 rental_order订单号和初始状态都放在 service 层处理。这里一定要在事务里操作避免预约状态更新成功却订单生成失败的情况。4.5 收藏与浏览记录收藏功能用 favorite 表存储用户和房源的关系。查询当前用户是否收藏过某个房源优先走 Redis 缓存缓存中没有再查数据库。浏览记录表主要用于“猜你喜欢”和后台统计写入成本低别过度设计。5. 后台管理系统与权限设计一套源码要落地不能只有前端接口后台管理是很多教程 Demo 最敷衍的部分但我认为这恰恰是项目完整实现的分水岭。一个只有 API 接口没有管理后台的系统没法真正演示给老师或者面试官看。5.1 基于角色的权限控制模型用户角色我用了简单但不简陋的模型USER、LANDLORD、ADMIN。租客和房东其实都是 USER 类型的扩展所以在 user 表里用一个 roles 字段存字符串数组比如USER,LANDLORD。这样同一个账号既能作为房东发布房源也能作为租客收藏房源。在 Spring Security 配置里用方法级注解控制接口访问PreAuthorize(hasRole(ADMIN)) PostMapping(/admin/house/audit) public Result auditHouse(RequestBody AuditRequest request) { return houseAuditService.audit(request); }这样只需要在自定义 UserDetailsService 中返回对应的权限列表剩余工作都由 Spring Security 完成不需要写一堆 if else 判断角色。5.2 房源审核流程的实现细节审核操作需要记录审核日志避免后续纠纷。我的实现方式是先更新 house 的 audit_status再插入 house_audit_log最后根据审核结果决定是否自动上架。如果审核通过status 变为 1如果拒绝status 仍然保持下架。前端页面要在拒绝理由里展示 comment 字段所以审核日志不能只写 action必须把 remark 一起存下来。5.3 管理后台数据统计面板统计面板用几条聚合 SQL 就能完成不需要引入额外报表组件。常见统计项包括-- 近7天新增房源数 SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM house WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time); -- 各城市房源排行 SELECT city_code, COUNT(*) AS cnt FROM house GROUP BY city_code ORDER BY cnt DESC;5.4 安全设计不是可有可无安全方面虽然不用做到银行级别但有一道底线接口防刷和参数校验。Redis 里做了一个简单的滑动窗口限流同一个手机号一分钟最多发 5 次验证码同一 IP 一小时内最多登录 20 次。前端表单提交的所有参数都加了 Valid 注解配合全局异常处理器统一返回错误信息。还有一个小细节管理后台接口不仅需要角色校验还应该加一个操作日志切面记录谁在什么时候对哪个房源做了审核。这样即使以后出现误操作也可以追溯。6. 打包部署与常见坑源码能编译只是开始项目开发完成只是第一步能打包部署到服务器跑起来才是完整项目实现。这一节我直接说部署流程和踩过的坑。6.1 Maven 打包与配置文件管理正式环境打包一般跳过测试mvn clean package -DskipTests打包完成后会在 rent-api 和 rent-admin 的 target 下生成 jar 包。我把配置文件按环境拆分成 application-dev.yml 和 application-prod.yml启动时通过spring.profiles.active指定环境数据库密码和 Redis 密码在 prod 配置里通过环境变量注入不写死。启动命令nohup java -jar rent-api.jar \ --spring.profiles.activeprod \ --spring.datasource.password${DB_PASSWORD} \ rent-api.log 21 6.2 Nginx 反向代理与前后端分离前端打包后的静态资源放在/usr/share/nginx/htmlAPI 请求通过 Nginx 反向代理到本机 8080 端口server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里要注意proxy_pass http://127.0.0.1:8080/;最后的斜杠它会把/api/login转发到后端的/login。如果没有斜杠则会把整个路径/api/login都转发过去Controller 里没有这个映射就会 404。6.3 上线后容易踩的三个坑第一个坑MyBatis-Plus 的逻辑删除和唯一索引冲突。我在 favorite 表加了唯一索引user_id, house_id同时启用了逻辑删除字段。用户取消收藏后记录被打上 deleted1这时再收藏同一房源数据库唯一索引还是能检测到旧记录导致插入失败。解决方法是把唯一索引改成user_id, house_id, deleted或者在取消收藏时物理删除该记录。第二个坑JWT 密钥不能随便改。我早期把密钥写在 application.yml 里每次更新配置重启后用户手里的旧 Token 全部失效因为签名算法用的是 HS256密钥不同导致解析失败。正确的做法是设置一个固定且足够长的密钥环境变量保证重启不变化。第三个坑Redis 缓存穿透。房源详情页如果一直查询不存在的房源 id每次都打到数据库容易被恶意刷接口。我后来加了空值缓存查不到时缓存一个空对象并设置短过期时间效果立竿见影。6.4 后续扩展方向这套源码的扩展空间很明确。按房源订阅让用户关注某个区域的新房源接第三方地图服务在地图上展示房源位置引入在线签约和支付后预约状态机可以升级成真正的租房交易状态机。数据库表结构已经预留了 order_no、start_date、end_date 字段加业务进去不用重构表。我个人实际做下来的体会是同城服务类系统的难点不在单个功能有多复杂而在于所有模块之间要有一条完整、可流转的链路。你光是能发布房源不够还要保证房源能被搜到、被预约、被审核、被管理。希望这套 Java 同城租房系统源码的拆解能给你一个完整的参照系写的时候少走弯路。最后一个小建议源码拿到后不要急着跑先把建表脚本逐行看一遍把表关系理清再去读业务代码学习效率会高很多。
