拓冰建站拓冰建站
首页 / 资讯中心 / 正文

同城租房系统实战:Spring Boot+Vue3前后端分离完整实现

做这个同城租房系统起因其实很实在——很多朋友在准备Java课程设计或者毕业设计时最头疼的不是写代码而是找不到一个业务逻辑完整、能真正跑起来的选题。同城租房系统恰恰是这种典型项目它的业务足够闭环从用户注册、房东发布房源、租客多条件筛选、在线预约看房到最终生成租房订单一条链路走下来Spring Boot、MyBatis、MySQL、Redis这些Java后端核心技术全部覆盖而且业务场景根本不用凭空想象打开任何一个租房App就能对照着设计功能。我这次整理的这个Java同城租房系统前后端分离后端基于Spring Boot 2.7 MyBatis Plus Redis前端用的Vue3 Element Plus实现了租客端、房东端、管理后台三端核心功能。整套源码结构完整可以直接跑。接下来我会把需求拆解、技术选型、数据库设计、核心模块实现、完整搭建过程和踩坑记录全部摊开来讲适合正在找课程设计项目的同学也适合想了解一个完整业务系统如何从零落地的初级开发者参考。1. 项目全景这套系统到底要做什么1.1 业务需求拆解“同城”是这个系统的核心关键词。它不是面向全国的房产平台而是强调本地化——用户注册时需要绑定所在城市房东发布房源时必须选择归属城市租客的每一次搜索、筛选、推荐都基于当前城市展开。这个定位和58同城、贝壳找房的同城策略一致也决定了它的数据模型必须围绕城市维度来设计。围绕这个核心我把系统拆成了三个角色和六大业务线租客端注册登录、浏览房源、多条件筛选、收藏房源、预约看房、确认租房、在线签订租房订单房东端发布房源、管理房源上下架、查看预约请求、确认或拒绝预约、查看租房订单管理后台房源审核、用户封禁/解封、城市管理、基础数据统计这里有一个容易被忽略的细节房源发布后不能直接展示必须经过管理后台审核。这个设计是我特意保留的它让系统的业务链多了一层状态流转也顺便满足了课程设计里“管理员角色”的常规要求。如果去掉审核环节系统就退化成普通的增删改查在答辩时很容易被问住。1.2 核心业务流转整套系统的核心流程可以用一段话描述清楚租客注册登录后在首页切换到自己所在城市进入房源列表页通过租金区间、户型、朝向、面积等条件组合筛选找到心仪的房源后进入详情页查看房屋实拍图、小区位置和房东信息点击“预约看房”填写期望看房时间。房东在个人中心看到预约请求后确认或拒绝。双方线下看房满意后租客在系统中确认租房系统自动生成租房订单并把该房源状态改为已出租防止重复下单。这个流程看起来简单但落到代码层面就涉及不少细节预约状态有“待确认、已确认、已拒绝、已完成、已取消”五种房源状态有“待审核、已上架、已下架、已出租”四种订单一旦生成房源状态就要联动变更。这些状态流转是整份代码里最值得研究的部分比单纯写CRUD有价值得多。1.3 项目目录结构完整项目的包结构大致如下rent-house/ ├── backend/ # 后端工程 │ ├── src/main/java/com/rent/ │ │ ├── controller/ # 控制层用户端、房东端、管理端 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis Plus Mapper接口 │ │ ├── entity/ # 实体类 │ │ ├── dto/ # 请求/响应对象 │ │ ├── config/ # 配置类跨域、Redis、MyBatisPlus │ │ ├── common/ # 统一返回结果、异常处理、工具类 │ │ └── interceptor/ # JWT拦截器 │ └── src/main/resources/ │ ├── application.yml # 核心配置 │ └── sql/ # 建表脚本与初始化数据 ├── frontend/ # 前端工程Vue3 Vite │ ├── src/ │ │ ├── api/ # 接口封装 │ │ ├── views/ # 页面组件 │ │ ├── router/ # 路由配置 │ │ └── store/ # 登录状态管理 └── README.md # 部署说明后端严格按三层架构分包Controller负责参数接收和响应封装Service承载业务逻辑Mapper只做数据访问。这样的分包方式在课程设计和实际生产项目中都是主流做法答辩时讲架构分层会非常加分。2. 技术选型为什么是 Spring Boot MyBatis Plus Redis2.1 后端技术栈与理由这个项目的后端选型我几乎没有犹豫直接用了一套最稳的组合组件版本选择理由Spring Boot2.7.xStarter机制大幅降低配置成本内置Tomcat社区资料多MyBatis Plus3.5.x单表CRUD免写SQL内置分页插件比原生MyBatis省一半代码量MySQL8.0.x关系型数据模型清晰配合Navicat可视化操作效率高Redis5.x缓存热点房源、存验证码解决高频查询压力JWT0.9.x无状态登录方案后端不存Session多端适配友好Lombok最新消除实体类getter/setter样板代码有些同学喜欢用Spring Cloud微服务那一套来做课程设计我的建议是没必要。同城租房系统的量级用单体应用完全够微服务带来的服务拆分、注册中心、网关配置反而会让整个项目复杂度暴涨答辩时如果对分布式原理讲不透反而容易露怯。把单体应用做深做扎实比堆砌技术栈更有说服力。2.2 前端方案选择前端我选了Vue3 Element Plus Axios Vite。选择Vue3而不是Vue2是因为组合式API写起来更简洁而且Vite的启动速度比Webpack快好几倍开发体验好很多。Element Plus的表格、表单、弹窗组件非常齐全管理后台的页面搭建基本就是拖组件的效率。Axios统一封装了请求拦截器和响应拦截器登录Token注入、统一错误提示这类逻辑都收敛在一个文件里。如果前端基础薄弱也可以把前端换成Thymeleaf服务端渲染后端一套代码直接输出页面。不过那样的话前后端分离的架构亮点就没有了我建议还是用Vue3哪怕只会套模板把Element Plus的组件改一改也能出效果。2.3 分层架构设计思路这里重点讲讲为什么必须分层。我见过不少课程设计代码所有的SQL和业务逻辑全堆在Controller里一个方法几百行看起来功能都实现了但一加需求就崩排查问题更是灾难。这套系统的分层方式是标准的Controller→Service→Mapper三层Controller层只干三件事接收参数、调用Service、返回统一结果。不写任何SQL和业务判断Service层承载全部业务逻辑校验参数、处理状态流转、调用多个Mapper完成事务操作Mapper层只做数据访问配合MyBatis Plus的BaseMapper单表操作零SQL这样的好处是每一层职责单一出了bug能快速定位到具体层。比如预约状态不对那一定在Service层数据查不出来大概率是Mapper或SQL的问题。团队协作时也可以并行开发互不阻塞。3. 数据库设计10张表撑起完整业务3.1 核心表结构详解数据库设计是整个项目的地基我建了10张表这里挑最核心的几张讲。用户表user是最基础的表字段上除了常规的账号密码还要记录用户角色和城市ID。角色用role字段区分1表示租客2表示房东管理员单独建表。密码存储用的是BCrypt加密不是MD5——MD5加盐也不好使彩虹表一查就破生产环境密码存储严禁用MD5这是我在实际项目里被安全评审怼过之后长记性的。房源表house字段比较多我重点讲几个关键点CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 房源ID, landlord_id BIGINT NOT NULL COMMENT 房东用户ID, city_id BIGINT NOT NULL COMMENT 所属城市ID, title VARCHAR(100) NOT NULL COMMENT 房源标题, address VARCHAR(255) NOT NULL COMMENT 详细地址, price DECIMAL(10,2) NOT NULL COMMENT 月租金, deposit DECIMAL(10,2) NOT NULL COMMENT 押金, house_type TINYINT NOT NULL COMMENT 户型1一室 2二室 3三室 4四室及以上, area DECIMAL(8,2) NOT NULL COMMENT 面积(平米), orientation TINYINT DEFAULT 0 COMMENT 朝向1东 2南 3西 4北 5南北通透, status TINYINT DEFAULT 0 COMMENT 状态0待审核 1已上架 2已下架 3已出租, description TEXT COMMENT 房源描述, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表;注意status字段我用一个TinyInt做状态机而不是用单独的字段区分审核状态和出租状态。原因很简单一个房源不可能既是“待审核”又是“已出租”这些状态本来就是互斥的用单一字段管理状态流转反而更清晰代码里只需要判断当前状态能不能迁移到目标状态避免出现逻辑漏洞。房源图片表house_image单独建表和房源表一对多关联。为什么不直接把图片路径存到房源表的一个字段里因为一个房源通常有5到10张实拍图如果用逗号拼接的字段来存查询时要拆分字符串维护时还要小心处理遗留的逗号非常难受。单独建表后用group by或列表查询天然支持多图后续加图删图也只是增删记录不影响房源主表结构。预约表appointment的核心字段是appointment_time期望看房时间、status状态、remark备注留言。订单表rent_order则要记录租客ID、房源ID、租金、押金、起租日期、到期日期以及最终的合同编号。这里有一个业务规则生成订单时系统根据起止月份自动计算总金额押一付三的时候总金额等于4个月租金。这个计算逻辑放在Service层做后端说了算前端只做展示。3.2 几个容易忽略的设计细节第一个细节是软删除。房源、订单这类核心业务数据不要物理DELETE我统一加了deleted字段配合MyBatis Plus的TableLogic注解实现逻辑删除。好处是数据可追溯管理员误操作还能恢复面试时聊到这点会很加分。第二个细节是城市表。城市信息单独建city表房源和用户都存city_id而不是直接存城市名字符串。直接存名字的好处是查询简单但代价是城市改名、数据一致性、统计分组全是坑单独建表之后前端城市下拉框的数据来源也有了着落。第三个细节是索引。我在city_id status上建了联合索引因为最频繁的查询是“某城市下所有已上架房源”这个索引能把查询效率提升一个量级。price字段也建了普通索引支撑租金区间查询。开发阶段数据量小感觉不到差别但答辩时能把索引设计讲清楚比写一堆CRUD有含金量得多。4. 核心功能实现与关键代码4.1 登录认证JWT无状态方案登录模块用JWT做无状态认证。流程是用户提交账号密码后端用BCrypt校验密码校验通过后生成Token返回前端前端把Token存到本地后续每个请求都通过拦截器注入Authorization请求头。下面是核心的Token工具类Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; // 生成Token public String generateToken(User user) { return Jwts.builder() .setSubject(user.getId().toString()) .claim(username, user.getUsername()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8)) .compact(); } // 解析Token返回用户ID public Long parseToken(String token) { Claims claims Jwts.parser() .setSigningKey(secret.getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }配合拦截器在preHandle里统一校验Tokenpublic class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; // 预检请求直接放行 } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { // 解析成功则放行并把用户ID放入request上下文 Long userId jwtUtils.parseToken(token.substring(7)); request.setAttribute(userId, userId); return true; } catch (Exception e) { // 解析异常走统一异常处理 } } response.setStatus(401); return false; } }这里有一个需要提醒的细节Token过期时间的设置。我设的是24小时对于课程设计够用但如果做成真实运营的系统建议改成2小时过期加RefreshToken机制。JWT是无状态的服务端没法主动吊销Token一旦泄露只能等它自然过期所以过期时间必须权衡安全性和用户体验。4.2 房源发布与图片上传房源发布是信息密集度最高的功能前端要提交标题、地址、价格、户型、面积、朝向、描述等十几个字段同时还要上传多张图片。这里最容易踩的坑是图片上传和表单提交的时序问题。我的做法是图片先独立上传到后端后端把图片存储在本地磁盘的上传目录返回图片的URL地址前端拿到URL后再把整个房源表单一起提交。这样表单提交就只有一个POST请求不会出现图片和字段数据不同步的问题。图片上传接口的核心逻辑PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件为空); } // 生成唯一文件名避免中文名乱码和覆盖 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; // 按日期分目录存储避免单个目录文件过多 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); File dir new File(uploadPath, datePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, fileName)); // 返回可访问的URL String url /upload/ datePath / fileName; return Result.success(url); }图片存储路径必须在application.yml里配置成绝对路径同时要做静态资源映射把本地上传目录映射成HTTP可访问的URL。很多同学的图片上传后返回了路径但浏览器访问404九成是静态资源映射没配。具体配置放到第5部分实操环节讲。顺带说一句文件类型校验不能省。我见过有人上传.php文件然后被服务器解析执行出事的案例虽然这只是一个课程设计项目但从一开始就养成校验文件类型的习惯是好事。我这里限制了jpg、jpeg、png、webp四种格式同时限制单张图片不超过5MB。4.3 多条件筛选查询筛选查询是租房系统的核心体验功能。租客在列表页勾选“三室、朝南、1500-2500元”后端要能组合查询。最直接的做法是写动态SQLMyBatis Plus的LambdaQueryWrapper就能优雅解决public PageResultHouseVO searchHouse(int page, int size, HouseQuery query) { PageHouse p new Page(page, size); LambdaQueryWrapperHouse wrapper new LambdaQueryWrapper(); // 城市必须传 wrapper.eq(House::getCityId, query.getCityId()); // 已上架才能展示待审核和下架的一律过滤 wrapper.eq(House::getStatus, 1); // 租金区间 if (query.getMinPrice() ! null) { wrapper.ge(House::getPrice, query.getMinPrice()); } if (query.getMaxPrice() ! null) { wrapper.le(House::getPrice, query.getMaxPrice()); } // 户型 if (query.getHouseType() ! null) { wrapper.eq(House::getHouseType, query.getHouseType()); } // 朝向 if (query.getOrientation() ! null) { wrapper.eq(House::getOrientation, query.getOrientation()); } // 面积区间 if (query.getMinArea() ! null) { wrapper.ge(House::getArea, query.getMinArea()); } // 按最新发布排序 wrapper.orderByDesc(House::getCreateTime); PageHouse result houseMapper.selectPage(p, wrapper); // 转VO填充图片、房东信息等 return convertToPageResult(result); }用LambdaQueryWrapper最大的好处是动态条件天然支持哪个条件为空就不拼接哪个不会出现手动拼接SQL时的空格和AND位置问题。这里也体现了为什么选MyBatis Plus而不是原生MyBatis——原生MyBatis做动态SQL得写XML里的if标签代码量翻倍可读性还差。4.4 预约看房的状态机设计预约模块是这个系统业务逻辑最复杂的地方。我前文提过预约有五种状态0待确认、1已确认、2已拒绝、3已完成、4已取消。这些状态不是随便能跳转的需要定义合法流转路径待确认 → 已确认房东同意待确认 → 已拒绝房东拒绝待确认 → 已取消租客主动取消已确认 → 已完成线下看房结束租客确认已确认 → 已取消看房前租客取消代码里我写了一个简单的状态校验防止非法流转private static final MapInteger, ListInteger APPOINTMENT_STATE_TRANSITIONS new HashMap(); static { APPOINTMENT_STATE_TRANSITIONS.put(0, Arrays.asList(1, 2, 4)); APPOINTMENT_STATE_TRANSITIONS.put(1, Arrays.asList(3, 4)); } public void updateStatus(Long appointmentId, Integer targetStatus, Long operatorId) { Appointment appointment appointmentMapper.selectById(appointmentId); if (appointment null) { throw new BizException(预约记录不存在); } ListInteger allowed APPOINTMENT_STATE_TRANSITIONS.get(appointment.getStatus()); if (allowed null || !allowed.contains(targetStatus)) { throw new BizException(非法的状态流转: appointment.getStatus() - targetStatus); } appointment.setStatus(targetStatus); appointmentMapper.updateById(appointment); }这个设计的价值在于把状态流转规则集中管理页面上的操作按钮根据当前状态动态显隐后端再做一次兜底校验双保险。我在实际开发中见过太多因为状态流转没控制好导致出现“已拒绝却又能确认看房”这类搞笑bug的项目大家写业务系统时一定要把状态机当作一等公民来对待。4.5 Redis缓存热点房源为什么要用Redis最直接的理由是首页的推荐房源和热点房源接口会被频繁访问每次都查MySQL压力大而且首页数据变化频率低非常适合缓存。缓存的逻辑是第一次查询时从MySQL读取数据写入Redis设置过期时间比如30分钟后续请求直接读缓存。为了保证数据一致性当管理员审核通过或房东修改房源时主动删除对应缓存让下一次查询重新回源数据库。public ListHouseVO getHotHouses(Long cityId) { String key rent:hot:city: cityId; // 尝试从缓存读取 String cached redisTemplate.opsForValue().get(key); if (StringUtils.hasText(cached)) { return JSON.parseArray(cached, HouseVO.class); } // 缓存未命中查数据库 ListHouseVO houses houseMapper.selectHotHouses(cityId); if (houses ! null !houses.isEmpty()) { // 写入缓存30分钟过期 redisTemplate.opsForValue().set(key, JSON.toJSONString(houses), 30, TimeUnit.MINUTES); } return houses; }缓存穿透和缓存雪崩的概念这里不展开但可以记住两个简单防御空值也缓存防止穿透过期时间加随机抖动防止雪崩。这两个点虽然简单面试时能说出来就是亮点。5. 从零到一完整实操搭建记录5.1 环境准备与版本搭配这个项目我实测过一套稳定可复现的环境组合照这个配置来踩坑最少JDK 1.8Spring Boot 2.7用JDK8完全没问题别贸然上17Maven 3.8MySQL 8.0Redis 5.0Node 16Vue3 Vite需要IDE后端IDEA前端WebStorm或VS CodeJDK版本这里必须提醒一句Spring Boot 2.7官方支持JDK8到JDK21但很多教程和依赖在JDK17下会有各种隐蔽的兼容问题课程设计阶段用JDK8最省心。如果你用的是JDK17运行时报“源发行版17需要目标发行版17”的经典错误时就是IDE编译级别和项目配置不一致导致的在IDEA里把Project Structure三处Java版本统一一下就好。5.2 后端项目初始化步骤后端我用IDEA直接初始化Spring Boot项目关键步骤如下第一步在start.spring.io或者IDEA内置的Spring Initializr里创建工程依赖勾选Spring Web、MyBatis Plus注意这里要用自定义坐标官方Initializr没有MyBatis Plus、MySQL Driver、Lombok、Redis。第二步修改pom.xml引入MyBatis Plus和JWT相关依赖。MyBatis Plus3.5.x用了独立的starter坐标千万别引错dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency第三步配置application.yml。这里我贴一份完整的关键配置照着改就能跑server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/rent_house?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl file: upload-path: D:/rent-house/upload/ jwt: secret: your-secret-key-please-change-me expire: 86400000第四步别忘了配置静态资源映射把本地上传目录映射到HTTP路径。在Spring Boot里实现WebMvcConfigurer接口重写addResourceHandlersConfiguration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }每次静态资源404先检查这一块。5.3 前端项目初始化前端用Vite创建Vue3工程命令很简单npm create vitelatest frontend -- --template vue cd frontend npm install npm install axios element-plus vue-router pinia npm run dev前端需要做的核心工作有几项路由配置登录页、首页、房源列表、房源详情、发布房源、我的预约等、Axios拦截器封装注入Authorization头、统一处理401跳转登录、Element Plus按需引入、状态管理用户信息。Axios拦截器是前端必写的样板代码直接决定联调效率axios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) axios.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )前端项目跑起来后默认端口是5173后端是8080跨域问题在第6部分详细讲。5.4 联调验证流程前后端都启动后我的验证顺序是按业务链路走的能最快暴露集成问题第一步先调“城市列表”接口验证后端是否正常启动、数据库连通、CORS是否生效。第二步注册一个新用户登录拿到Token验证JWT签发和拦截器放行逻辑。第三步以房东身份发布一条房源上传3张图片确认图片URL可访问。第四步以管理员身份登录把这条房源审核通过。第五步切换租客身份搜索刚才的房源发起预约再回到房东端确认预约。第六步租客确认租房检查订单是否生成、房源状态是否联动变成已出租。这一套链路全部跑通说明项目的主干功能是健康的。6. 实战踩坑与排查技巧实录6.1 跨域问题前后端分离第一道坎就是跨域。前端5173端口访问后端8080端口浏览器会拦截非同源的请求。我用的解决方案是后端加CORS全局配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有一个特别容易忽略的坑当后端的JWT拦截器拦截请求时对于浏览器的OPTIONS预检请求必须直接放行否则前端会一直报跨域错误而真实原因其实是拦截器把预检请求给拦截掉了。我前文拦截器代码里第一行就处理了这个问题这个顺序不能反。6.2 图片上传后无法访问这个问题很经典图片成功传到了本地磁盘重新开启服务后上传的图片全部404。原因是Spring Boot默认把项目目录当临时目录开发环境下重启后会变导致上传文件指向的路径已经不存在了。解决办法在上文已经提到把上传路径配置成固定绝对路径而不是依赖相对路径同时在配置类里做静态资源映射。如果用的是Windows注意路径末尾要带斜杠Linux服务器部署时uploadPath配成类似于/data/rent-upload/这样的独立目录别忘了给目录设置写权限。6.3 时间字段格式化异常后端返回的LocalDateTime默认序列化格式是“2025-01-01T12:00:00”前端要显示成人话的“2025-01-01 12:00:00”还得额外处理。我在项目里统一加了Jackson的全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8注意时区问题MySQL连接串里serverTimezone必须配Asia/Shanghai否则查出来的时间会比实际少8小时。这个问题我在第一次联调时就踩了时间全部对不上一度以为是数据库存错了。6.4 MyBatis Plus分页插件失效很多人引入MyBatis Plus后直接调用selectPage方法发现分页不生效或者查出来的数据还是全量。原因是MyBatis Plus的分页插件需要在配置类里显式注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不加这段selectPage虽然不报错但SQL里不会拼接LIMIT语句相当于分了个寂寞。这个坑特别隐蔽因为代码执行完全正常只是结果不对。排查思路是看控制台打印的SQL日志如果发现没有LIMIT关键字基本就是插件没注册。6.5 事务不生效的坑生成租房订单这个操作涉及三步插入订单记录、修改房源状态为已出租、扣减房源库存如果有。这三步必须在一个事务里任何一步失败都要回滚。我在开发中踩过的坑是Service内部方法自调用导致事务不生效。场景是这样的——我在一个类里写了createOrder方法它在内部直接调用了本类的updateHouseStatus方法结果updateHouseStatus跑在另一个事务里或者完全没有事务。Spring的声明式事务是基于AOP代理的同类内部方法调用不会经过代理Transactional自然就失效了。解决办法有两个一是把需要事务的方法拆到不同Service类通过Bean调用二是用AopContext.currentProxy()获取当前代理对象再调用。课程设计项目遇到事务相关bug时先怀疑是不是自调用问题这是最常见的根因。7. 我的经验总结与扩展方向做完这个同城租房系统我个人最深的体会是一个业务项目的难点从来不在单个技术点而在把这些技术点有机串联起来的业务流程设计。比如预约的状态机、房源的状态联动、订单生成的幂等性每一个单拎出来都不难但组合在一起就需要全局视角。这也是为什么我特别推荐大家用“同城租房”这类生活化选题做项目——业务场景不需要虚构需求天然完整技术覆盖又足够广。最后分享几个我实测下来的建议第一先设计数据库再写代码数据库字段设计好了后面几乎不用大改第二接口返回统一封装成Result对象前后端联调时字段名称不一致的问题会少很多第三代码里多写注释尤其是状态流转和业务规则部分答辩时你翻着注释讲比现场读代码流畅得多。如果学有余力这个项目还有几个很不错的扩展方向引入ElasticSearch做房源全文搜索替代现在的MySQL模糊查询用WebSocket做房东和租客的在线聊天替代线下电话沟通接入第三方地图API展示房源位置。这些扩展方向每个都够做一个独立的提升模块把简历上的项目亮点直接拉满。整套源码的部署说明和SQL脚本我放在了项目的README里照着跑完整个链路你基本就能把这个类型项目的所有套路吃透了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门