在实际 Java Web 项目开发中,构建一个功能完整、结构清晰的业务系统是检验技术综合运用能力的重要方式。租房平台作为一个典型的业务系统,涉及用户管理、房源信息展示、订单处理、支付对接等多个模块,非常适合作为 Spring Boot 技术栈的实战项目。它不仅要求开发者掌握 Spring Boot 的快速开发能力,还需要整合数据库、前端模板、持久层框架以及处理异步交互等常见问题。本文将围绕一个基于 Spring Boot 的租房平台项目,从零开始,详细拆解其技术选型、环境搭建、核心模块实现、关键问题排查以及生产环境注意事项,旨在为希望深入理解 Spring Boot 项目实战的开发者提供一份可复现、可学习的完整指南。
1. 技术栈选型与项目初始化
一个典型的 Spring Boot 租房平台项目,其技术栈的选择直接决定了开发效率和系统稳定性。基于常见的工程实践,我们选择一套成熟、稳定且社区活跃的技术组合。
1.1 核心框架与依赖
后端以 Spring Boot 2.7.x 作为基础框架,这是一个经过长期验证的稳定版本,在功能、性能和社区支持上达到了很好的平衡。数据库操作使用 MyBatis-Plus,它极大地简化了单表 CRUD 操作,同时保留了 MyBatis 的灵活性。前端视图层采用 Thymeleaf 模板引擎,它能够与 Spring Boot 无缝集成,支持自然模板语法,适合快速开发服务端渲染的页面。项目管理工具使用 Maven,负责依赖管理和构建流程。
以下是项目初始化时pom.xml中的核心依赖配置:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>rental-platform</artifactId> <version>1.0.0</version> <name>rental-platform</name> <description>基于Spring Boot的租房平台</description> <properties> <java.version>11</java.version> </properties> <dependencies> <!-- Web 支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- 数据持久化 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 数据校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- 常用工具 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>关键解释:
spring-boot-starter-parent统一管理了大量依赖的版本,避免了版本冲突。mybatis-plus-boot-starter版本需要与 Spring Boot 2.7.x 兼容,3.5.x 是一个稳定选择。lombok设置为optional,并在打包插件中排除,是为了避免将 Lombok 的编译时依赖打入最终的生产包。- Java 版本设置为 11,这是一个长期支持版本,兼顾了稳定性和现代语言特性。
1.2 数据库设计与初始化
租房平台的核心数据模型通常包括用户、房源、订单等。这里给出一个简化的数据库表结构设计。
用户表 (user):
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '密码(加密后)', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色:0-管理员,1-普通用户', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';房源表 (house):
CREATE TABLE `house` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '房源ID', `title` varchar(200) NOT NULL COMMENT '房源标题', `description` text COMMENT '房源描述', `price` decimal(10,2) NOT NULL COMMENT '月租金', `address` varchar(500) NOT NULL COMMENT '详细地址', `city` varchar(50) NOT NULL COMMENT '城市', `district` varchar(50) NOT NULL COMMENT '区域', `landlord_id` bigint(20) NOT NULL COMMENT '房东用户ID', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-待审核,1-可租,2-已租出,3-已下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_landlord` (`landlord_id`), KEY `idx_city_district` (`city`,`district`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房源信息表';订单表 (order):
CREATE TABLE `order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` varchar(64) NOT NULL COMMENT '订单编号', `house_id` bigint(20) NOT NULL COMMENT '房源ID', `tenant_id` bigint(20) NOT NULL COMMENT '租客用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-待支付,1-已支付,2-已取消,3-已完成', `start_date` date NOT NULL COMMENT '租赁开始日期', `end_date` date NOT NULL COMMENT '租赁结束日期', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_house` (`house_id`), KEY `idx_tenant` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';设计要点:
- 主键与索引:
id使用AUTO_INCREMENT自增主键。为外键字段(如landlord_id,house_id,tenant_id)和常用查询字段(如city,district)建立索引,以提升查询性能。 - 金额字段:
price和total_amount使用DECIMAL(10,2)类型,避免浮点数精度问题。 - 状态字段:使用
tinyint表示各种状态,并在代码中使用枚举类进行映射,提高可读性。 - 时间字段:
create_time和update_time使用数据库的CURRENT_TIMESTAMP功能自动维护。
1.3 项目基础结构
使用 IDE(如 IntelliJ IDEA)或 Spring Initializr 创建项目后,建议遵循以下目录结构组织代码,这有助于保持清晰的分层架构:
src/main/java/com/example/rental/ ├── RentalPlatformApplication.java # 启动类 ├── config/ # 配置类 ├── controller/ # 控制层 ├── service/ # 业务逻辑层 │ └── impl/ # 业务逻辑实现层 ├── mapper/ # MyBatis-Plus Mapper接口层 ├── entity/ # 实体类(对应数据库表) ├── dto/ # 数据传输对象 ├── vo/ # 视图对象 └── util/ # 工具类 src/main/resources/ ├── application.yml # 主配置文件 ├── static/ # 静态资源(CSS, JS, 图片) └── templates/ # Thymeleaf模板文件2. 核心功能模块实现
接下来,我们将实现用户管理、房源展示和订单处理这三个核心模块。每个模块都遵循 Controller -> Service -> Mapper 的分层模式。
2.1 用户注册与登录
用户模块是系统的入口。首先创建用户实体类User,并使用 MyBatis-Plus 的注解进行映射。
package com.example.rental.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; // 存储加密后的密码 private String phone; private String email; private Integer role; // 0-管理员,1-普通用户 @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }创建对应的 Mapper 接口UserMapper,继承 MyBatis-Plus 的BaseMapper,即可获得基础的 CRUD 方法。
package com.example.rental.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.rental.entity.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper<User> { }在 Service 层,我们需要处理业务逻辑,例如密码加密。这里使用 Spring Security 的BCryptPasswordEncoder,它是一个专门用于密码哈希的工具。
package com.example.rental.service; import com.example.rental.entity.User; public interface UserService { boolean register(User user); User login(String username, String password); }package com.example.rental.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.rental.entity.User; import com.example.rental.mapper.UserMapper; import com.example.rental.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserMapper userMapper; private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @Override public boolean register(User user) { // 1. 基础校验 if (user == null || !StringUtils.hasText(user.getUsername()) || !StringUtils.hasText(user.getPassword())) { return false; } // 2. 检查用户名是否已存在 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, user.getUsername()); if (userMapper.selectCount(wrapper) > 0) { return false; } // 3. 密码加密 user.setPassword(passwordEncoder.encode(user.getPassword())); // 4. 设置默认角色(普通用户) user.setRole(1); // 5. 插入数据库 return userMapper.insert(user) > 0; } @Override public User login(String username, String password) { if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { return null; } // 1. 根据用户名查询用户 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); User user = userMapper.selectOne(wrapper); // 2. 验证用户存在且密码匹配 if (user != null && passwordEncoder.matches(password, user.getPassword())) { // 登录成功,注意返回前清空密码 user.setPassword(null); return user; } return null; } }关键点:
@RequiredArgsConstructor是 Lombok 注解,为final字段生成构造函数,实现依赖注入。BCryptPasswordEncoder每次加密会生成不同的盐值,因此必须使用其matches方法进行验证,而不是直接比较字符串。- 查询用户是否存在时,使用
selectCount比selectOne更轻量。登录成功后,返回的用户对象应清除密码字段,避免敏感信息泄露。
最后,在 Controller 层提供 RESTful 接口。为了简化,这里使用 Session 来管理登录状态。
package com.example.rental.controller; import com.example.rental.entity.User; import com.example.rental.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpSession; @Controller @RequestMapping("/user") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping("/register") public String registerPage() { return "user/register"; // 对应 templates/user/register.html } @PostMapping("/register") public String doRegister(User user, HttpSession session) { boolean success = userService.register(user); if (success) { session.setAttribute("loginUser", userService.login(user.getUsername(), user.getPassword())); return "redirect:/"; // 注册成功并自动登录,跳转到首页 } else { // 可以添加错误信息到Model中,这里简化处理 return "redirect:/user/register?error=1"; } } @GetMapping("/login") public String loginPage() { return "user/login"; } @PostMapping("/login") public String doLogin(String username, String password, HttpSession session) { User user = userService.login(username, password); if (user != null) { session.setAttribute("loginUser", user); return "redirect:/"; } else { return "redirect:/user/login?error=1"; } } @GetMapping("/logout") public String logout(HttpSession session) { session.invalidate(); return "redirect:/"; } }2.2 房源信息管理
房源管理涉及房东发布房源和管理员审核房源。首先创建房源实体House和对应的HouseMapper。
房源服务HouseService需要实现分页查询、条件筛选(如按城市、区域、价格区间)、发布和状态更新等功能。这里展示分页查询和条件筛选的实现。
package com.example.rental.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.rental.entity.House; import com.example.rental.mapper.HouseMapper; import com.example.rental.service.HouseService; import com.example.rental.vo.HouseQueryVO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor public class HouseServiceImpl implements HouseService { private final HouseMapper houseMapper; @Override public IPage<House> queryHousePage(HouseQueryVO queryVO, Long pageNum, Long pageSize) { Page<House> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<House> wrapper = new LambdaQueryWrapper<>(); // 构建动态查询条件 if (StringUtils.hasText(queryVO.getCity())) { wrapper.eq(House::getCity, queryVO.getCity()); } if (StringUtils.hasText(queryVO.getDistrict())) { wrapper.eq(House::getDistrict, queryVO.getDistrict()); } if (queryVO.getMinPrice() != null) { wrapper.ge(House::getPrice, queryVO.getMinPrice()); } if (queryVO.getMaxPrice() != null) { wrapper.le(House::getPrice, queryVO.getMaxPrice()); } // 默认只查询状态为“可租”的房源 wrapper.eq(House::getStatus, 1); // 排序:按更新时间倒序 wrapper.orderByDesc(House::getUpdateTime); return houseMapper.selectPage(page, wrapper); } // 其他方法:发布房源、更新状态、根据ID查询详情等... }HouseQueryVO是一个视图对象,用于接收前端传递的查询条件。
package com.example.rental.vo; import lombok.Data; import java.math.BigDecimal; @Data public class HouseQueryVO { private String city; private String district; private BigDecimal minPrice; private BigDecimal maxPrice; }在 Controller 中,我们将分页查询结果和查询条件传递给 Thymeleaf 模板。
package com.example.rental.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.example.rental.entity.House; import com.example.rental.service.HouseService; import com.example.rental.vo.HouseQueryVO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller @RequestMapping("/house") @RequiredArgsConstructor public class HouseController { private final HouseService houseService; @GetMapping("/list") public String list(HouseQueryVO queryVO, @RequestParam(defaultValue = "1") Long pageNum, @RequestParam(defaultValue = "10") Long pageSize, Model model) { IPage<House> page = houseService.queryHousePage(queryVO, pageNum, pageSize); model.addAttribute("page", page); model.addAttribute("query", queryVO); // 将查询条件回显到页面 return "house/list"; } }对应的 Thymeleaf 模板templates/house/list.html片段,展示如何使用分页数据和条件回显:
<!-- 搜索表单 --> <form th:action="@{/house/list}" method="get"> <input type="text" name="city" th:value="${query.city}" placeholder="城市"> <input type="text" name="district" th:value="${query.district}" placeholder="区域"> <input type="number" name="minPrice" th:value="${query.minPrice}" placeholder="最低价"> <input type="number" name="maxPrice" th:value="${query.maxPrice}" placeholder="最高价"> <button type="submit">搜索</button> </form> <!-- 房源列表 --> <div th:each="house : ${page.records}"> <h3 th:text="${house.title}">房源标题</h3> <p>地址:<span th:text="${house.address}"></span></p> <p>价格:<span th:text="${#numbers.formatDecimal(house.price, 0, 2)}">0</span> 元/月</p> <!-- 更多信息 --> </div> <!-- 分页控件 --> <div> <span>共 <span th:text="${page.total}">0</span> 条记录</span> <a th:href="@{/house/list(pageNum=1, city=${query.city}, district=${query.district}, minPrice=${query.minPrice}, maxPrice=${query.maxPrice})}">首页</a> <a th:if="${page.hasPrevious()}" th:href="@{/house/list(pageNum=${page.current-1}, city=${query.city}, district=${query.district}, minPrice=${query.minPrice}, maxPrice=${query.maxPrice})}">上一页</a> <span th:text="${page.current}"></span>/<span th:text="${page.pages}"></span> <a th:if="${page.hasNext()}" th:href="@{/house/list(pageNum=${page.current+1}, city=${query.city}, district=${query.district}, minPrice=${query.minPrice}, maxPrice=${query.maxPrice})}">下一页</a> <a th:href="@{/house/list(pageNum=${page.pages}, city=${query.city}, district=${query.district}, minPrice=${query.minPrice}, maxPrice=${query.maxPrice})}">末页</a> </div>2.3 订单创建与状态流转
订单模块是业务的核心,涉及库存(房源状态)的锁定、金额计算和状态机管理。首先创建订单实体Order和 Mapper。
订单创建是一个事务性操作,需要确保房源状态从“可租”变为“已租出”与创建订单这两个步骤的原子性。这里使用 Spring 的@Transactional注解来管理事务。
package com.example.rental.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.example.rental.entity.House; import com.example.rental.entity.Order; import com.example.rental.entity.User; import com.example.rental.mapper.HouseMapper; import com.example.rental.mapper.OrderMapper; import com.example.rental.service.OrderService; import com.example.rental.vo.OrderCreateVO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.UUID; @Service @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { private final OrderMapper orderMapper; private final HouseMapper houseMapper; @Override @Transactional(rollbackFor = Exception.class) public Order createOrder(OrderCreateVO createVO, User tenant) { // 1. 验证房源存在且可租 House house = houseMapper.selectById(createVO.getHouseId()); if (house == null || house.getStatus() != 1) { // 状态1代表可租 throw new RuntimeException("房源不存在或不可租"); } // 2. 计算租赁天数与总金额 (简化计算:按月租金*月数) long months = ChronoUnit.MONTHS.between( createVO.getStartDate().withDayOfMonth(1), createVO.getEndDate().withDayOfMonth(1) ) + 1; // 至少一个月 BigDecimal totalAmount = house.getPrice().multiply(BigDecimal.valueOf(months)); // 3. 生成唯一订单号 String orderNo = "ORD" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); // 4. 创建订单对象 Order order = new Order(); order.setOrderNo(orderNo); order.setHouseId(createVO.getHouseId()); order.setTenantId(tenant.getId()); order.setTotalAmount(totalAmount); order.setStatus(0); // 待支付 order.setStartDate(createVO.getStartDate()); order.setEndDate(createVO.getEndDate()); // 5. 更新房源状态为“已租出” LambdaUpdateWrapper<House> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(House::getId, house.getId()) .eq(House::getStatus, 1) // 乐观锁:只有当前状态是可租时才更新 .set(House::getStatus, 2); // 状态2代表已租出 int updateCount = houseMapper.update(null, updateWrapper); if (updateCount == 0) { // 更新失败,可能房源状态已被其他请求修改,抛出异常触发事务回滚 throw new RuntimeException("房源状态已变更,创建订单失败"); } // 6. 插入订单记录 orderMapper.insert(order); return order; } // 其他方法:支付成功回调、取消订单、查询订单列表等... }关键点:
@Transactional(rollbackFor = Exception.class):确保方法内任何异常(包括RuntimeException和Exception)都会导致事务回滚,保持数据一致性。- 乐观锁:在更新房源状态时,使用
eq(House::getStatus, 1)作为条件。如果并发请求导致该条件不成立,则更新行数为0,我们可以感知到并发冲突并回滚订单创建。 - 订单号生成:采用“前缀+时间戳+随机码”的方式生成唯一订单号,避免重复和猜测。
- 金额计算:这里进行了简化。实际项目中,租金计算可能涉及按天计费、服务费、押金等更复杂的逻辑。
3. 关键配置与前端交互
3.1 应用配置文件
application.yml是 Spring Boot 的核心配置文件,需要正确配置数据库、MyBatis-Plus 和 Thymeleaf。
server: port: 8080 servlet: context-path: /rental spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/rental_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: your_username password: your_password thymeleaf: cache: false # 开发时关闭缓存,修改模板立即生效 prefix: classpath:/templates/ suffix: .html mode: HTML encoding: UTF-8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,生产环境应关闭 map-underscore-to-camel-case: true # 自动将下划线命名转为驼峰 global-config: db-config: id-type: auto # 主键自增 logic-delete-field: isDeleted # 全局逻辑删除字段(如果表中有此字段) logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值3.2 使用 AJAX 实现异步交互
在房源详情页,用户点击“立即预订”按钮时,通常不希望页面刷新。这时可以使用 AJAX 提交表单数据到后端,并根据返回结果动态更新页面。
首先,在页面中引入 jQuery(或其他 JS 库)来简化 AJAX 操作。
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>然后,编写 JavaScript 函数处理预订按钮的点击事件。
function submitOrder() { var formData = { houseId: $('#houseId').val(), startDate: $('#startDate').val(), endDate: $('#endDate').val() // 其他表单数据... }; $.ajax({ url: '/order/create', type: 'POST', contentType: 'application/json', data: JSON.stringify(formData), success: function(response) { if (response.code === 200) { alert('订单创建成功!订单号:' + response.data.orderNo); // 可以跳转到订单详情页或我的订单列表 window.location.href = '/order/my'; } else { alert('创建失败:' + response.msg); } }, error: function(xhr) { alert('请求异常,请稍后重试'); console.error(xhr); } }); }对应的 Controller 方法需要返回统一的 JSON 格式。
package com.example.rental.controller; import com.example.rental.entity.Order; import com.example.rental.entity.User; import com.example.rental.service.OrderService; import com.example.rental.vo.OrderCreateVO; import com.example.rental.vo.R; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; @RestController @RequestMapping("/api/order") @RequiredArgsConstructor public class OrderApiController { private final OrderService orderService; @PostMapping("/create") public R<Order> createOrder(@RequestBody OrderCreateVO createVO, HttpSession session) { User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { return R.error(401, "用户未登录"); } try { Order order = orderService.createOrder(createVO, loginUser); return R.ok(order); } catch (RuntimeException e) { return R.error(500, e.getMessage()); } } }R是一个通用的响应包装类。
package com.example.rental.vo; import lombok.Data; @Data public class R<T> { private Integer code; private String msg; private T data; public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> R<T> error(Integer code, String msg) { R<T> r = new R<>(); r.setCode(code); r.setMsg(msg); return r; } }4. 项目运行、问题排查与生产建议
4.1 项目启动与验证
- 启动应用:在 IDE 中直接运行
RentalPlatformApplication的main方法,或在项目根目录下执行mvn spring-boot:run。 - 访问首页:打开浏览器,访问
http://localhost:8080/rental/。如果配置了server.servlet.context-path,不要遗漏。 - 验证数据库连接:观察控制台日志,如果没有报数据库连接错误,并且能看到 MyBatis-Plus 的 banner 信息,通常表示连接成功。
- 功能验证:按顺序测试用户注册、登录、浏览房源、创建订单等核心流程。
4.2 常见问题与排查路径
在开发过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
启动时报Failed to configure a DataSource | 数据库配置错误或驱动未加载 | 1. 检查application.yml中的url,username,password。2. 检查 pom.xml中 MySQL 驱动依赖是否存在。3. 检查数据库服务是否启动。 | 修正配置,确保依赖和数据库服务正常。 |
访问页面报Whitelabel Error Page(404) | 请求路径错误或 Controller 未扫描到 | 1. 检查浏览器地址栏路径是否与@RequestMapping匹配。2. 检查启动类是否在 Controller 的上级包中。 3. 检查 @Controller或@RestController注解是否添加。 | 使用 IDE 的“Find Usages”功能查看映射路径,确保启动类位置正确。 |
| Thymeleaf 模板语法不生效,页面显示原始表达式 | 模板引擎配置错误或缓存问题 | 1. 检查spring.thymeleaf.prefix和suffix配置。2. 开发环境设置 spring.thymeleaf.cache=false。3. 检查 HTML 文件头是否有 xmlns:th="http://www.thymeleaf.org"。 | 确认配置,清理浏览器缓存,或尝试重启应用。 |
| MyBatis-Plus 查询不到数据或字段映射失败 | 实体类字段与数据库列名不匹配 | 1. 检查@TableName,@TableId,@TableField注解是否正确。2. 开启 SQL 日志 ( log-impl: StdOutImpl),查看实际执行的 SQL 和参数。3. 确认数据库表中字段是否存在。 | 根据日志调整实体类注解或数据库表结构。 |
事务@Transactional不生效 | 方法非 public,或异常被捕获未抛出 | 1. 确保方法是public。2. 确保异常传播到了事务管理器(默认只回滚 RuntimeException和Error)。3. 检查是否在同一个类内部调用事务方法(代理失效)。 | 使用@Transactional(rollbackFor = Exception.class),或将事务方法放到另一个 Service 中调用。 |
| AJAX 请求返回 403 (Forbidden) | 未处理 CSRF 令牌或 Spring Security 拦截 | 1. 如果引入了 Spring Security,默认会开启 CSRF 防护。 2. 检查请求头 Content-Type是否为application/json。 | 对于纯 API 请求,可以在 Security 配置中禁用 CSRF 对特定路径的防护,或手动添加 CSRF Token。 |
4.3 生产环境部署与优化建议
将学习项目推向生产环境,需要考虑更多因素:
配置外置化:将数据库密码、第三方密钥等敏感信息从
application.yml移到环境变量或配置中心。可以使用spring.config.import支持外部配置文件。# application-prod.yml spring: datasource: url: ${DB_URL} username: ${DB_USER} password: ${DB_PASS}通过
-Dspring.profiles.active=prod激活生产配置。连接池与性能:默认的 HikariCP 连接池性能很好,但需要根据数据库负载调整参数。
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000日志与监控:
- 使用 Logback 或 Log4j2 替换默认的 Logback,配置按级别和大小滚动日志文件。
- 集成 Spring Boot Actuator 暴露健康检查、指标等信息,方便接入 Prometheus 和 Grafana。
- 关键业务操作(如创建订单、支付回调)必须打印业务日志,包含唯一流水号,便于链路追踪。
安全性加固:
- 密码加密:必须使用
BCryptPasswordEncoder等强哈希算法,切勿使用 MD5 或 SHA-1。 - SQL 注入:坚持使用 MyBatis-Plus 的 Wrapper 或
#{}参数绑定,杜绝字符串拼接 SQL。 - XSS 防护:Thymeleaf 默认会对 HTML 内容进行转义。对于富文本内容,需要进行安全的 HTML 过滤。
- 越权访问:在 Controller 方法中,必须校验当前登录用户是否有权操作目标资源(例如,只能取消自己的订单)。
- 密码加密:必须使用
打包与部署:
- 使用
mvn clean package -DskipTests打包,生成可执行的 JAR 文件。 - 考虑使用 Docker 容器化部署,确保环境一致性。编写
Dockerfile:FROM openjdk:11-jre-slim COPY target/rental-platform-1.0.0.jar app.jar ENTRYPOINT ["java", "-jar", "/app.jar"] - 在容器编排或服务器上,配置好 JVM 内存参数(如
-Xmx512m)。
- 使用
这个基于 Spring Boot 的租房平台项目,涵盖了从技术选型、数据库设计、分层开发、事务管理到前端交互的完整流程。真正理解一个项目,不仅要能跑通 Demo,更要清楚每一步背后的设计考量、潜在的问题以及如何让它变得更健壮。建议你在实现基础功能后,尝试加入图片上传、在线支付、消息通知、缓存(如 Redis)等更复杂的模块,并思考如何设计更完善的权限系统和后台管理功能,这会让你的工程能力得到更全面的锻炼。