简介基于SpringBoot与Vue的智能租房系统完整项目属于前后端分离的全栈实践面向毕业设计、课程设计与期末大作业场景也适合想掌握主流Web开发流程的学习者。压缩包共565个文件大小约55.77MB包含116个java后端文件、97个js前端文件、69个dart移动端文件以及png/svg界面资源、xml/json配置和Markdown文档可同时覆盖Web端、移动端与后端服务。目前已有22人学习/浏览。项目目录按Mobile/Web、Back-end和Docs划分模块清晰后端采用RESTful API与前端通信负责数据存储、用户身份验证、业务逻辑处理和交易数据安全前端提供房源浏览、预约看房、房源发布和租约管理等操作界面并支持网页端与移动端不同设备访问文档部分记录系统架构、模块划分、接口定义、开发流程与常见问题解决方案。开发过程遵循最佳实践采用迭代模式代码可读性与可维护性较好这套资源既能直接支撑二次开发也可作为SpringBoot、Vue及跨端开发方法的完整参考。1. 拿到「基于 SpringBoot 的智能租房系统」压缩包先想清楚这三件事基于 SpringBoot 的智能租房系统是 Java 后端里出现频率最高的题目组合之一毕业设计、实训作业、小团队给中介做的内部工具用的都是同一套骨架房东发布房源租客按城市、预算、户型筛房子预约看房后走状态流转管理员负责上下架审核。标题里的「智能」多数实现不接大模型而是用条件权重算推荐分把匹配度高、热度高的房源顶到前面。收到这类 zip 项目先想清楚三件事表结构有没有给状态流转和并发留好字段检索条件变多以后查询怎么拼不乱多人同时抢一个看房时段怎么保证不超卖。这三个问题定了剩下的 Controller 和页面只是工作量问题。下面按我接手这类 SpringBoot 项目的习惯顺序从数据建模写到部署验证代码可以直接改。2. SpringBoot 租房系统的数据建模从房源到订单的关系设计2.1 实体关系先理清再动手建库大多数「智能租房系统」都用一张用户表同时承载房东和租客两种身份用 role 字段区分。我一般把用户表设计为只存账号、密码密文、手机号和角色身份相关的扩展资料比如房东的实名信息、租客的通勤偏好字段不多就冗余在用户表里不要过早拆 profile 表。拆表越多Mapper 层和事务边界越复杂对这个体量没有收益。房源表和预约表是另外两条主链。房源表围绕「一套房的一次挂牌」建模注意是挂牌记录而不是物理房源——同一套房子到期后重新挂牌应该另建一条记录这样历史成交价和带看记录都能追溯。预约表是租客和房东之间的履约凭证它的状态流转是 Service 层最需要保护的东西。把关系画清楚house 对 appointment 是一对多user 对 house 是一对多appointment 同时关联 house、renter 和 landlord。2.2 建表 SQL字段类型、默认值与唯一约束下面这份 SQL 是这类项目常用的底稿去掉了权限、菜单等扩展表只留核心链路。落库前定两个规则金额用「分」不用「元」避免浮点误差状态字段用 TINYINT 加注释不要用字符串枚举。-- 用户表房东与租客同表用 role 区分 CREATE TABLE sys_user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL COMMENT BCrypt 密文禁止明文, phone VARCHAR(20) DEFAULT , role TINYINT NOT NULL DEFAULT 1 COMMENT 1房东 2租客 3管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 房源表一次挂牌一条记录到期重新挂牌就再加一条 CREATE TABLE house ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, landlord_id BIGINT UNSIGNED NOT NULL, title VARCHAR(100) NOT NULL, city VARCHAR(32) NOT NULL, district VARCHAR(32) NOT NULL, address VARCHAR(200) NOT NULL, price_cents INT UNSIGNED NOT NULL COMMENT 月租金单位分, area_sqm DECIMAL(6,2) NOT NULL, bedroom_count TINYINT NOT NULL DEFAULT 1, livingroom_count TINYINT NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 2下架 3已出租, score INT NOT NULL DEFAULT 0 COMMENT 推荐权重分, visit_count INT NOT NULL DEFAULT 0 COMMENT 被约看次数, expire_at DATETIME NOT NULL COMMENT 挂牌到期时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_city_district_price (city, district, price_cents), KEY idx_status_score (status, score DESC) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表; -- 预约表同一租客对同一房源同一时段只能有一条 CREATE TABLE appointment ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, house_id BIGINT UNSIGNED NOT NULL, renter_id BIGINT UNSIGNED NOT NULL, landlord_id BIGINT UNSIGNED NOT NULL COMMENT 冗余房东ID方便按房东查待确认, visit_time DATETIME NOT NULL COMMENT 预约看房时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已取消 3已完成, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_house_renter_time (house_id, renter_id, visit_time), KEY idx_landlord_status (landlord_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约看房表;建表逻辑说明house 表把 status 和 score 组成联合索引首页热门列表的查询是WHERE status 1 ORDER BY score DESC这个索引能同时覆盖过滤和排序避免 filesort。appointment 表的唯一键防止同一个租客对同一房源同一时间提交多条预约是防重复的第一道闸。price_cents 用 INT UNSIGNED上限约 4294 万元对租房场景绰绰有余如果算成交总价要换 BIGINT。expire_at 由 Service 层在发布时写入后面定时任务做自动下架就依赖这个字段。2.3 字段设计里最常见的两个坑第一个坑是金额用 DOUBLE 或 Java 的 float。房源价格、押金一旦参与浮点运算显示的 6800 元可能变成 6799.9999。把金额存成 integer 分展示层除以 100JSON 输出也干净是我在这个场景下的默认做法。第二个坑是时间字段混用。MySQL 5.7 之后 DATETIME 支持默认值和ON UPDATE CURRENT_TIMESTAMP但连接串里必须配serverTimezoneAsia/Shanghai否则 JDBC 驱动按 JVM 时区解析差八小时的故障排查起来非常痛苦。下表是这三张表里建议锁死的字段约定新加字段时先对照一遍。约定项取值规则原因金额integer单位分避免浮点误差JSON 输出干净状态TINYINT 注释占空间小比字符串稳定时间DATETIME serverTimezoneAsia/Shanghai避免时区偏移问题删除逻辑删除字段 deleted房源和订单需要追溯不物理删版本号version INT DEFAULT 0预约确认走乐观锁必须有逻辑删除在 MyBatis-Plus 里配置logic-delete-field: deleted之后会自动生效查询统一带deleted 0。这个字段对租房业务很有必要租客取消预约、房东下架房源都不该真的删数据后台审计要看完整履约过程。建表阶段留好它比上线后补省事得多。3. 用 SpringBoot 落地核心业务房源发布、检索与预约3.1 分层约定Controller 只做参数接收业务写在 ServiceSpringBoot 项目好不好接手先看包结构。我习惯分 controller、service、mapper、entity、dto、config 六层controller 不写任何业务逻辑只做参数绑定、JSR-303 校验和结果包装。这样做的直接收益是拿到压缩包想找「房源发布到底做了什么」只需要打开 HouseServiceImpl不用在多层之间来回跳。新手常犯的错误是在 controller 里直接调 mapper事务完全失效预约确认写一半报错状态就悬在中间。这里有个体现 SpringBoot 自动装配特点的细节类上标了Service且构造器只有一个参数时Spring 会走构造器注入不需要Autowired。新代码统一用构造器注入单元测试时直接 new 一个 Service 传 mock 即可不用反射改字段。3.2 房源发布接口DTO 校验与事务边界发布房源是写操作里最典型的一条链路。DTO 用 validation 注解做第一道校验Service 层做业务校验数据库约束兜底。下面是发布接口的最小实现。// HouseController.java RestController RequestMapping(/house) public class HouseController { private final HouseService houseService; public HouseController(HouseService houseService) { this.houseService houseService; } PostMapping public Long publish(Valid RequestBody HousePublishCommand cmd) { return houseService.publish(cmd); } } // HouseServiceImpl.java 中的核心方法 Override Transactional(rollbackFor Exception.class) public Long publish(HousePublishCommand cmd) { // 1. 从登录上下文取房东 ID不允许前端传 landlordId防止越权挂到别人名下 Long landlordId SecurityUtils.getCurrentUserId(); House house new House(); BeanUtils.copyProperties(cmd, house); house.setLandlordId(landlordId); house.setStatus(HouseStatus.ON_SALE.getCode()); house.setScore(computeInitScore(cmd)); // 初始推荐分定时任务再刷新 // 2. expireAt 与当前时间比较防止写入过去的时间 if (house.getExpireAt().isBefore(LocalDateTime.now())) { throw new BusinessException(挂牌到期时间不能早于当前时间); } houseMapper.insert(house); return house.getId(); }这段代码里三个点要交代清楚。第一Transactional(rollbackFor Exception.class)必须显式声明 rollbackFor否则只有 RuntimeException 触发回滚受检异常会让事务提交半截数据。第二landlordId 一定从服务端上下文取前端传什么角色 ID 都不可信这是这类系统越权漏洞最常见的入口SecurityUtils 内部一般是从 ThreadLocal 或 Redis 会话里读登录态。第三DTO 校验管格式Service 校验管业务规则比如到期时间不能在过去两层各管各的。3.3 多条件检索LambdaQueryWrapper 的拼接逻辑租客搜索页通常有城市、区域、价格区间、户型、朝向五六个筛选项「智能」体现在筛选后的排序上。用 MyBatis-Plus 的 LambdaQueryWrapper 拼条件核心技巧是「条件为 null 就不拼」。public PageHouse search(SearchQuery query) { LambdaQueryWrapperHouse wrapper Wrappers.lambdaQuery(House.class); wrapper.eq(House::getCity, query.getCity()) .eq(StringUtils.hasText(query.getDistrict()), House::getDistrict, query.getDistrict()) .ge(query.getMinPriceCents() ! null, House::getPriceCents, query.getMinPriceCents()) .le(query.getMaxPriceCents() ! null, House::getPriceCents, query.getMaxPriceCents()) .ge(query.getMinBedroom() ! null, House::getBedroomCount, query.getMinBedroom()) .eq(House::getStatus, HouseStatus.ON_SALE.getCode()) .orderByDesc(House::getScore) .orderByDesc(House::getVisitCount); return houseMapper.selectPage( new Page(query.getPageNum(), query.getPageSize()), wrapper); }注意.eq、.ge、.le的第一个参数是布尔条件为 false 时 MyBatis-Plus 自动跳过该条件省掉一长串 if-else。排序上把 score 放第一位让推荐分高的房源排在前面visit_count 作为第二排序反映热度。分页用 Page 对象配合拦截器自动生成 LIMIT不用手写分页 SQL。SearchQuery 里的 pageNum 和 pageSize 要设上限pageSize 最大 50防止前端传 10000 一次拖垮数据库。3.4 预约下单状态机与重复提交防护预约看房是这套系统里状态最复杂的操作。很多毕设项目用一个字段硬记状态没有任何约束最后出现「已取消的预约被确认」这种脏数据。正确做法是画状态机待确认只能转已确认或已取消已确认只能转已完成非法迁移在 Service 层直接拒绝。Override Transactional(rollbackFor Exception.class) public void bookVisit(Long houseId, LocalDateTime visitTime) { // 1. 幂等检查同一租客对同一房源同一时间只能有一条预约 Long count appointmentMapper.selectCount( Wrappers.AppointmentlambdaQuery() .eq(Appointment::getHouseId, houseId) .eq(Appointment::getRenterId, SecurityUtils.getCurrentUserId()) .eq(Appointment::getVisitTime, visitTime)); if (count ! null count 0) { throw new BusinessException(您已预约该时段请勿重复提交); } // 2. 房源必须处于上架状态 House house houseMapper.selectById(houseId); if (house null || house.getStatus() ! HouseStatus.ON_SALE.getCode()) { throw new BusinessException(房源不可预约); } // 3. 落库version 字段留给后续确认/取消操作使用 Appointment appointment new Appointment(); appointment.setHouseId(houseId); appointment.setRenterId(SecurityUtils.getCurrentUserId()); appointment.setLandlordId(house.getLandlordId()); appointment.setVisitTime(visitTime); appointment.setStatus(AppointmentStatus.WAIT_CONFIRM.getCode()); appointmentMapper.insert(appointment); houseMapper.update(null, Wrappers.HouselambdaUpdate() .setSql(visit_count visit_count 1) .eq(House::getId, houseId)); }这里的幂等依赖两层应用层 selectCount 拦住绝大多数重复请求数据库唯一索引uk_house_renter_time兜底两个请求同时穿过时后到者撞唯一键抛异常。setSql让 visit_count 在数据库端自增避免先查后改的并发覆盖。到这一步预约单只是待确认状态真正的并发冲突出现在房东确认和租客取消同时发生时放到下一章配合缓存一起讲。4. 缓存与并发控制Redis 在智能租房系统里的调优4.1 热门房源列表为什么必须走缓存首页热门房源和推荐列表是查询压力最大的接口租客每次刷新首页都会打过来。即便有idx_status_score索引也架不住首页尖峰流量。常见做法是把推荐列表的房源 ID 缓存到 Redis设置 10 到 30 分钟过期过期后第一个请求回源数据库再回填也就是 Cache-Aside 模式。缓存里存 ID 列表而不是整个 JSON列表接口只需要 ID详情的大字段没必要占缓存内存。Service public class RecommendService { private static final String HOT_HOUSE_KEY rental:hot:house; private final StringRedisTemplate redisTemplate; private final HouseMapper houseMapper; public RecommendService(StringRedisTemplate redisTemplate, HouseMapper houseMapper) { this.redisTemplate redisTemplate; this.houseMapper houseMapper; } public ListHouse topHot(int topN) { // 1. 先读 Redis 里的 ID 列表 ListString ids redisTemplate.opsForList() .range(HOT_HOUSE_KEY, 0, topN - 1); if (ids ! null !ids.isEmpty()) { // 2. 按 ID 批量查库再按缓存中的顺序重新组装 MapLong, House houseMap houseMapper.selectBatchIds(ids) .stream().collect(Collectors.toMap(House::getId, h - h)); return ids.stream() .map(id - houseMap.get(Long.valueOf(id))) .filter(Objects::nonNull) .collect(Collectors.toList()); } // 3. 未命中则回源数据库按推荐分取前 N 条 ListHouse houses houseMapper.selectList( Wrappers.HouselambdaQuery() .eq(House::getStatus, HouseStatus.ON_SALE.getCode()) .orderByDesc(House::getScore) .last(LIMIT topN)); // 4. 回填缓存并设置 10 分钟过期 ListString idList houses.stream() .map(h - String.valueOf(h.getId())) .collect(Collectors.toList()); if (!idList.isEmpty()) { redisTemplate.opsForList().rightPushAll(HOT_HOUSE_KEY, idList); redisTemplate.expire(HOT_HOUSE_KEY, Duration.ofMinutes(10)); } return houses; } }这里用 List 而不是 Set 存 ID是因为 list 天然有序score 排名本身就是顺序读出来不用再排。selectBatchIds 返回的集合不保证顺序所以要按缓存里的 ID 顺序重新组装这一步新手很容易漏漏了首页房源顺序会随机跳动。回填时先 rightPushAll 再 expire保证不会出现写了一半就过期的情况。.last(LIMIT topN)存在注入风险topN 必须是代码常量或内部传入绝不能接受前端参数拼接。4.2 Redis 连接池与序列化的参数设置Spring Boot 2.x 和 3.x 默认都用 Lettuce 作为 Redis 客户端。很多项目报Unable to connect to Redis不是 Redis 挂了而是连接池太小或超时太短。下面是 application.yml 里的一组起点参数。spring: data: redis: host: localhost port: 6379 password: ${REDIS_PASSWORD:} timeout: 3s lettuce: pool: max-active: 16 # 最大连接数按接口 QPS 调整 max-idle: 8 # 最大空闲连接 min-idle: 2 # 最少保持的空闲连接预热用 max-wait: 3s # 拿不到连接的最大等待时间这组参数的逻辑max-active 16 对单机系统足够Redis 本身是单线程处理命令连接开太多反而增加上下文切换min-idle 设 2 是为了避免突发流量来了才现建连接max-wait 3 秒意味着高峰期等不到连接就直接报错返回而不是无限阻塞拖垮整个 SpringBoot 线程池。序列化方面如果用了 RedisTemplate 而不是 StringRedisTemplate必须在配置类里把 key 的序列化器改成 StringRedisSerializer否则 key 会带\xAC\xED前缀缓存不命中时第一眼就该查这个。4.3 预约确认的并发控制乐观锁与 Redis 锁怎么选房东确认预约和租客取消预约可能同时发生。典型场景房东在后台点了确认同一秒租客在手机端取消两个请求都读到 status0各自更新成期望值后提交的覆盖先提交的状态机就乱了。对于低频写操作乐观锁成本最低Transactional(rollbackFor Exception.class) public void confirm(Long appointmentId, Long landlordId) { Appointment appointment appointmentMapper.selectById(appointmentId); if (appointment null) { throw new BusinessException(预约不存在); } if (!appointment.getLandlordId().equals(landlordId)) { throw new BusinessException(无权操作该预约); } // 状态机校验待确认才能转已确认 if (appointment.getStatus() ! AppointmentStatus.WAIT_CONFIRM.getCode()) { throw new BusinessException(当前状态不可确认); } // 乐观锁UPDATE 条件带上 version影响行数为 0 说明已被其他请求修改 int rows appointmentMapper.update(null, Wrappers.AppointmentlambdaUpdate() .set(Appointment::getStatus, AppointmentStatus.CONFIRMED.getCode()) .set(Appointment::getVersion, appointment.getVersion() 1) .eq(Appointment::getId, appointmentId) .eq(Appointment::getVersion, appointment.getVersion())); if (rows 0) { throw new BusinessException(预约状态已变化请刷新后重试); } }乐观锁的前提是表里有 version 字段建表时已经留好。UPDATE 把version 旧值放进 WHERE数据库行锁保证同一时刻只有一个事务能改成功另一个 affected rows 为 0业务层抛提示。它适合「读多写少、冲突少」的场景预约确认一天几百次完全够用。如果需求变成「同一时段只能被一个租客约中」那就得用 Redis setnx 做前置占位再落单占位 key 要带 5 秒过期时间防止应用宕机把时段锁死。我习惯把两种方案分开用状态变更走乐观锁时段抢占走 Redis 锁不要动不动上分布式锁框架那是杀鸡用牛刀。4.4 推荐分计算先跑通规则再谈模型「智能推荐」起步阶段用不上算法一个可解释的加权公式就够。常用公式是score 基础分 价格匹配分 热度分因子来源权重基础分房源资料完整度30价格匹配与同小区均价差越接近越高40热度visit_count 按对数缩放到 0~3030计算逻辑放在 Service 里由定时任务全量刷新新发布的房源用初始分占位避免列表里出现 score0 排到最底的情况。这个公式的好处是向租客解释「为什么推荐这套」时三个因子都能说清后期想换向量召回只需要替换 score 的来源对外接口不变。5. 围绕 SpringBoot 的部署与验证配置清单和排查顺序5.1 压缩包导入三步走打开 zip 先别急着跑。第一步看 pom.xml 确定 SpringBoot 版本。2.7.x 对应 JDK 8 到 173.x 强制 JDK 17 起如果你本机是 JDK 8 而项目是 3.x直接升级 JDK 比硬降 SpringBoot 版本省事得多3.x 底层依赖变动很大强退版本会撞上一堆兼容性问题。第二步建数据库按文件名顺序执行 sql 目录里的脚本字符集用 utf8mb4。第三步改 application.yml 里的数据源和 Redis 地址然后mvn spring-boot:run启动。常见的启动失败基本逃不出下面这张表报错信息原因处理方式Failed to configure a DataSource数据源配置缺失或 YAML 缩进错误检查 yml 层级与 spring.datasource 依赖Unable to connect to Redis地址、密码不对或连接池耗尽先排查网络连通性再调连接池参数Invalid bound statementMapper XML 没被扫描到检查 MapperScan 包路径Table doesnt exist建表脚本没执行或执行顺序错按依赖顺序重新执行 SQL注意在 IDE 里启动看到Failed to configure a DataSource大概率不是数据库没启动而是 application.yml 缩进错了。YAML 层级错误是 SpringBoot 配置问题里最隐蔽的一种先确认spring:下面每一层都对齐。5.2 application.yml 的必调参数server: port: 8080 servlet: context-path: /rental # 统一前缀方便反向代理区分多个服务 spring: datasource: url: jdbc:mysql://localhost:3306/rental_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: rental password: ${DB_PASSWORD:rental123} hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 jackson: time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 management: endpoints: web: exposure: include: health,info各参数说明数据源 URL 里 serverTimezone 必须显式指定Spring Boot 3.x 的 MySQL 驱动在时区缺失时直接报错密码用${DB_PASSWORD:rental123}环境变量占位压缩包项目写死密码可以理解但要意识到这是上线前的隐患。map-underscore-to-camel-case负责把price_cents自动映射到priceCents没有它查出来的对象全是 null。log-impl配 StdOutImpl 能在开发期看到完整 SQL 和参数排查问题最直接上线前要换掉否则 SQL 全量刷日志。配置项多了以后可以用ConfigurationProperties绑定到一个配置类比散落的Value好维护。5.3 Actuator 端点暴露控制SpringBoot 的 Actuator 是排查线上问题的利器但默认配置如果暴露 heapdump、threaddump 这类端点攻击者可以直接下载堆转储文件从里面挖出数据源密码、Token 等敏感信息。生产环境只保留 health 和 info 就够了。# 确认端点可见性只应看到 health 和 info curl -s http://localhost:8080/rental/actuator | jq ._links | keysSwagger 接口文档同理开发环境方便调试生产环境要么关闭要么加访问控制。压缩包项目最常见的交付问题就是生产环境开着 swagger-ui 和 heapdump这两项在部署清单里必须检查。5.4 冒烟验证一组 curl 把主链路过一遍# 1. 分页搜索上架房源 curl -s http://localhost:8080/rental/house/search?city上海pageNum1pageSize10 | jq .records | length # 2. 发布一套房源字段交给 DTO 校验 curl -s -X POST http://localhost:8080/rental/house \ -H Content-Type: application/json \ -d {title:徐汇两居室,city:上海,district:徐汇区,priceCents:680000,bedroomCount:2,expireAt:2026-12-31T23:59:59} | jq # 3. 预约看房 curl -s -X POST http://localhost:8080/rental/appointment \ -H Content-Type: application/json \ -d {houseId:1,visitTime:2025-08-20T10:00:00}验证顺序有讲究先测搜索确认数据源和表结构正常再测发布确认事务和参数校验生效最后测预约确认状态机和唯一约束起作用。如果项目接了登录拦截器这些 curl 要加-H Authorization: Bearer token开发阶段一般先在配置里放行 /house/search 这类只读接口。预约接口返回 500 时优先看日志里最后一条 SQL——95% 的情况是表字段和实体类对不上尤其注意逻辑删除字段 deleted 是否在实体里声明了MyBatis-Plus 的全局逻辑删除要求实体必须有对应字段。6. 一个值得试的进阶技巧用 SpringBoot 定时任务自动流转房源状态6.1 到期自动下架与热度分刷新「智能」二字除了推荐排序还有一层隐藏需求是系统自维护。房源挂三个月没人租不能一直占着列表位热度分跟着预约量走不能发布后永远不变。SpringBoot 自带的 Scheduled 足够解决这两件事不需要引入额外的任务调度中间件。先在主类上开启调度SpringBootApplication EnableScheduling public class RentalApplication { public static void main(String[] args) { SpringApplication.run(RentalApplication.class, args); } }然后写一个独立的 Job 组件把任务逻辑和业务 Service 分开Component Slf4j public class HouseStatusJob { private final HouseMapper houseMapper; public HouseStatusJob(HouseMapper houseMapper) { this.houseMapper houseMapper; } // 每天凌晨 2 点跑一次把超过 expire_at 的上架房源统一转下架 Scheduled(cron 0 0 2 * * ?) public void autoOffShelf() { int rows houseMapper.update(null, Wrappers.HouselambdaUpdate() .set(House::getStatus, HouseStatus.OFF_SALE.getCode()) .eq(House::getStatus, HouseStatus.ON_SALE.getCode()) .lt(House::getExpireAt, LocalDateTime.now())); log.info(定时下架过期房源 {} 条, rows); } // 每 30 分钟刷新一次热度分fixedDelay 表示上次执行完再等 30 分钟 Scheduled(fixedDelay 30 * 60 * 1000L, initialDelay 60 * 1000L) public void refreshScore() { ListHouse onSaleHouses houseMapper.selectList( Wrappers.HouselambdaQuery() .eq(House::getStatus, HouseStatus.ON_SALE.getCode())); for (House house : onSaleHouses) { int score ScoreRule.calculate(house); houseMapper.update(null, Wrappers.HouselambdaUpdate() .set(House::getScore, score) .eq(House::getId, house.getId())); } log.info(全量刷新房源推荐分完成共 {} 套, onSaleHouses.size()); } }两个任务代表了 Scheduled 的两种形态cron 表达式适合「每天固定时刻」的批量维护凌晨 2 点避开业务高峰fixedDelay 适合状态收敛类任务30 分钟一次initialDelay让应用启动后先等 1 分钟避免连接池没预热就全量刷分。全量刷分在小数据量下可以逐条 update超过五万条要改成分批处理每批 500 条防止一次更新锁太多行拖慢主库。验证任务是否按预期工作最直接的方式是把 cron 临时改成0 * * * * ?每分钟一次观察日志输出的下架条数确认无误再改回凌晨执行。score 只影响推荐列表排序不影响已成交订单的历史记录所以全量重算没有副作用。如果刷新过程中应用被手动停掉事务回滚不会留下半更新状态——前提是任务方法也标注了Transactional这也是定时任务代码里容易被忽略的一个前提。本文还有配套的精品资源点击获取