
简介这是一套面向高校计算机相关专业学生与Java初学者的大作业级项目源码采用SpringBoot后端与Vue前端分离架构围绕汽车租赁业务场景实现车辆管理、用户权限、订单处理等核心模块适合作为课程设计、毕业设计参考或全栈入门练手项目。压缩包共150个文件约1.18MB其中57个java源文件承载后端业务逻辑与Shiro权限控制19个xml与2个yaml、2个properties负责框架与数据源配置另有26张jpg、25张png图片资源及15个class编译文件整体结构完整、层次分明。目前已有146人学习下载。项目经导师指导认可并获98分高分代码经过严格调试可稳定运行读者可从中获取完整的工程目录组织方式、前后端接口对接思路、权限认证与数据库配置范例以及可直接复用的实体类、控制器与服务层实现便于快速理解企业级项目开发流程并在此基础上二次扩展。1. 汽车租赁系统从零到一为什么 SpringBoot Vue 是当下最稳的起步组合汽车租赁这个业务场景表面看就是“选车、下单、取车、还车”四步但真正落到代码里库存状态、押金冻结、违章押金、超时计费、门店异店还车每一个都能把新手项目拖进泥潭。我见过太多人用纯前端或者纯后端硬扛最后卡在“这辆车到底还能不能租”这种状态同步上。基于 SpringBoot Vue 的汽车租赁系统核心价值就在于把“车辆状态机”和“订单生命周期”拆成前后端各管一段后端用 SpringBoot 做事务和状态流转前端用 Vue 做实时筛选和表单交互。这套组合适合两类人一是课程设计或毕设需要完整业务闭环的学生二是想练手 RESTful 接口 前端工程化的初级开发者。它不追求高并发但能把租车业务里最典型的“资源占用与释放”逻辑讲透。2. 车辆状态机与订单生命周期先想清楚再写代码2.1 为什么车辆表不能只存一个 status 字段很多教程教你在 vehicle 表里加一个 status 字段0 表示可租1 表示已租。真跑起来第一天就会翻车用户 A 下单但未支付车辆该不该锁用户 B 同时点进详情页看到还是“可租”也下了单结果两个订单指向同一辆车。血泪经验是车辆状态必须和订单状态解耦用“时间区间占用”来判断而不是一个瞬时布尔值。常见做法是建一张 rental_order 表记录 vehicle_id、start_time、end_time、order_status。车辆是否可租由查询“该车在目标时间段内是否存在有效订单”决定。有效订单包括待支付、已支付、已取车三种状态取消和已完成则释放占用。这样即使两个用户同时下单数据库层面用唯一索引或乐观锁也能兜住。-- 车辆表只存基础信息不存实时状态 CREATE TABLE vehicle ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(16) NOT NULL UNIQUE, brand VARCHAR(32), daily_price DECIMAL(10,2), store_id BIGINT ); -- 订单表承载时间区间和状态 CREATE TABLE rental_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, vehicle_id BIGINT NOT NULL, user_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, order_status TINYINT COMMENT 0待支付 1已支付 2已取车 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_vehicle_time (vehicle_id, start_time, end_time) );上面这段 SQL 的关键在于 idx_vehicle_time 这个联合索引。查某辆车在某个时间段是否被占用时条件写成start_time 目标end AND end_time 目标start索引能直接命中。order_status 不放在索引里是因为状态变更频繁放进去反而增加维护成本。参数上daily_price 用 DECIMAL 而不是 FLOAT避免金额计算出现 0.30000000000000004 这种玄学问题。2.2 下单接口的幂等与锁用 Redis 还是数据库唯一索引下单是汽车租赁系统里最容易出并发问题的地方。我一般会分两层做第一层用 Redis 的 setnx 对vehicle_id start_time做短时锁防止同一毫秒内重复提交第二层在数据库用唯一索引兜底。但注意Redis 锁不是必须的如果团队没有 Redis 运维能力直接靠数据库唯一索引也能扛住中小规模。// SpringBoot 下单核心逻辑简化版 Transactional public OrderVO createOrder(Long vehicleId, Long userId, Date start, Date end) { // 1. 检查时间区间是否被占用 int count orderMapper.countActiveOrders(vehicleId, start, end); if (count 0) { throw new BizException(该时间段车辆已被预订); } // 2. 插入订单数据库唯一索引兜底 RentalOrder order new RentalOrder(); order.setVehicleId(vehicleId); order.setUserId(userId); order.setStartTime(start); order.setEndTime(end); order.setOrderStatus(0); try { orderMapper.insert(order); } catch (DuplicateKeyException e) { throw new BizException(手慢了该时间段刚被预订); } return convert(order); }这段代码里countActiveOrders 的 SQL 就是前面说的区间重叠查询。DuplicateKeyException 捕获的是数据库唯一索引冲突前提是你需要在 rental_order 表上建一个UNIQUE KEY uk_vehicle_time (vehicle_id, start_time, end_time)。但这里有个坑如果两个订单时间区间部分重叠但起止时间不完全一样唯一索引拦不住。所以更稳妥的做法是用 Redis 分布式锁或者把时间粒度切细比如按天占用。我一般会建议新手先用“按天预订”把 start_time 和 end_time 都归一化到日期唯一索引就能生效。2.3 前端 Vue 的车辆筛选与实时状态展示Vue 这边最容易被忽略的是“用户看到的状态”和“数据库真实状态”之间的延迟。比如用户 A 刚取消订单用户 B 的列表页还显示“已租”。常见做法是列表页不展示实时状态只展示基础信息用户点进详情页时再调一次可用性接口。这样既减少列表接口压力又避免状态不一致带来的客诉。// Vue 详情页检查可用性 async checkAvailability(vehicleId, start, end) { const res await axios.get(/api/order/check, { params: { vehicleId, start, end } }); this.available res.data.available; if (!this.available) { this.$message.warning(该时间段已被预订请调整时间); } }参数说明vehicleId 从路由参数取start 和 end 用 ISO 8601 格式传给后端避免时区解析差异。前端不要自己算“有没有被占用”只做展示判断逻辑全部放后端。这样即使前端被篡改也不会出现超卖。3. 从零搭出可运行的前后端骨架目录、依赖与联调3.1 SpringBoot 项目结构别把 Controller 当 Service 用我见过太多汽车租赁系统的 SpringBoot 代码Controller 里直接写 SQL 和业务判断最后改一个计费规则要翻三个文件。标准做法是 controller / service / mapper 三层DTO 和 Entity 分开。目录结构建议这样src/main/java/com/rental/ controller/ OrderController, VehicleController service/ OrderService, VehicleService mapper/ OrderMapper, VehicleMapper entity/ RentalOrder, Vehicle dto/ OrderCreateDTO, VehicleQueryDTO config/ WebConfig, RedisConfig依赖方面SpringBoot 版本不要追最新2.7.x 或 3.0.x 都行但要注意 3.0 之后 javax 包变成 jakarta很多老教程的代码直接复制会报错。MyBatis-Plus 用来做单表 CRUD 很省事但区间重叠查询还是手写 SQL 更可控。Lombok 看团队习惯如果 IDE 没装插件编译会报错新手建议先不用。!-- pom.xml 关键依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency版本号这里写的是我本地跑通的组合你如果遇到springboot版本太高导致的兼容问题优先降 SpringBoot 到 2.7.18这是目前最稳的过渡版本。3.2 Vue 项目初始化路由和请求封装先做好Vue 这边用 Vue CLI 或 Vite 都行Vite 启动快但如果你要参考的教程比较老用 Vue CLI 更省心。初始化之后先做两件事配路由和封装 axios。路由用 history 模式但部署时要在 Nginx 加 try_files否则刷新 404。// src/utils/request.js import axios from axios const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( res res.data, err { if (err.response err.response.status 401) { // 跳转登录 } return Promise.reject(err) } ) export default servicebaseURL 在开发环境用/api配合 vue.config.js 里的 proxy 转发到 SpringBoot 的 8080 端口。这样前端代码里不用写完整域名部署时也不用改。注意vue打包放进springboot中这个需求如果要把 Vue 打包产物放进 SpringBoot 的 static 目录axios 的 baseURL 要改成/并且 SpringBoot 要配好静态资源映射。3.3 前后端联调跨域和登录态怎么处理开发阶段跨域用 SpringBoot 的CrossOrigin或者全局配置都行但生产环境不要开跨域用 Nginx 同源代理。登录态我一般用 JWT前端存 localStorage后端用拦截器校验。汽车租赁系统里用户和管理员权限要分开管理员能看所有订单普通用户只能看自己的。// 全局跨域配置仅开发环境 Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }allowCredentials(true) 和 allowedOriginPatterns() 一起用时SpringBoot 2.4 之后不能用 allowedOrigins()必须用 allowedOriginPatterns否则启动报错。这个坑我踩过查了半天才发现是版本差异。4. 避坑与排查汽车租赁系统里最容易翻车的 5 个点4.1 时间区间重叠查询写反了边界现象用户预订 1 号到 3 号另一个人预订 3 号到 5 号系统提示不冲突结果 3 号当天被两个人同时占用。原因区间重叠判断用了start_time end AND end_time start把边界相等也算成不重叠。解决如果业务上取车和还车当天算同一天边界要算重叠条件写成start_time end AND end_time start反而会漏掉相等情况。正确做法是先明确业务规则还车日是否可租。我一般把时间归一化到天用start_date end AND end_date start并且把还车日也算占用。4.2 订单取消后车辆没释放现象用户取消订单列表页还是显示“已租”。原因取消接口只改了 order_status但查询可用性的 SQL 没有过滤取消状态。解决countActiveOrders 里必须加order_status IN (0,1,2)把已取消和已完成排除。同时检查是否有定时任务在扫过期未支付订单如果没有待支付订单会一直占用车辆。4.3 Vue 路由刷新 404现象开发环境正常打包部署后刷新页面报 404。原因history 模式下前端路由由 JavaScript 控制刷新时浏览器直接请求服务器路径服务器找不到对应文件。解决Nginx 配置try_files $uri $uri/ /index.html;或者在 Vue Router 里改用 hash 模式。如果打包放进 SpringBoot需要加一个 ErrorController 转发到 index.html。4.4 SpringBoot 版本太高导致依赖不兼容现象用 SpringBoot 3.x 搭配老版本 MyBatis-Plus启动报jakarta.servlet找不到。原因SpringBoot 3.0 把 javax.servlet 换成了 jakarta.servlet很多第三方库还没适配。解决要么降 SpringBoot 到 2.7.x要么把 MyBatis-Plus 升到 3.5.3 以上。我一般建议新手直接用 2.7.18生态最全教程最多。4.5 金额计算用 double 导致对不上账现象日租金 199.9租 3 天前端显示 599.7后端算出 599.6999999999999。原因浮点数精度丢失。解决数据库用 DECIMAL(10,2)Java 用 BigDecimal前端展示时用 toFixed(2)。BigDecimal 做除法时要指定保留位数和舍入模式否则除不尽会抛 ArithmeticException。5. 把系统跑稳之后三个能直接加分的进阶技巧5.1 用定时任务自动关闭超时未支付订单汽车租赁系统里用户下单后 15 分钟不支付订单应该自动取消并释放车辆。SpringBoot 的Scheduled就能做不需要引入额外中间件。Component public class OrderTimeoutTask { Autowired private OrderMapper orderMapper; // 每 5 分钟执行一次 Scheduled(cron 0 0/5 * * * ?) public void closeTimeoutOrders() { Date deadline new Date(System.currentTimeMillis() - 15 * 60 * 1000); orderMapper.closeOrdersBefore(deadline); } }对应的 SQL 是UPDATE rental_order SET order_status 4 WHERE order_status 0 AND create_time #{deadline}。注意 cron 表达式里0/5表示从 0 秒开始每 5 分钟别写成*/5在分钟位那个是每 5 分钟但起点不固定。这个任务跑之前确认数据库连接池够用别和业务查询抢连接。5.2 车辆可用性查询加缓存但别缓存状态车辆基础信息可以缓存比如品牌、日租金、门店这些不常变。但可用性状态不要缓存或者缓存时间极短比如 5 秒。我一般用 Spring Cache Rediskey 用vehicle:info:{id}过期时间 10 分钟。可用性查询直接走数据库因为区间重叠查询本身不慢加了缓存反而容易脏读。Cacheable(value vehicleInfo, key #vehicleId) public VehicleVO getVehicleInfo(Long vehicleId) { return vehicleMapper.selectById(vehicleId); }这里Cacheable的 value 对应 Redis 里的前缀key 用 SpEL 表达式。注意如果返回对象改了字段缓存不会自动更新需要在更新接口加CacheEvict。5.3 用状态机枚举替代魔法数字订单状态用 0、1、2、3、4 写死在代码里过两个月自己都忘了 2 是啥。定义一个枚举把状态流转规则也写进去。public enum OrderStatus { PENDING(0, 待支付), PAID(1, 已支付), PICKED(2, 已取车), FINISHED(3, 已完成), CANCELLED(4, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public static boolean isActive(int code) { return code PENDING.code || code PAID.code || code PICKED.code; } }这样查询可用性时直接调OrderStatus.isActive(status)比写status IN (0,1,2)可读性好得多。如果以后加“已还车待结算”状态只改枚举和 isActive 方法不用满项目搜数字。最后说个我自己的习惯每次改完订单状态相关的代码一定手动跑一遍“下单→支付→取车→还车→完成”全流程再跑一遍“下单→取消”和“下单→超时关闭”。汽车租赁系统的坑大多不在代码多难而在状态流转的边界没覆盖全。希望帮到你。本文还有配套的精品资源点击获取