news 2026/11/5 19:08:19

SpringBoot+Vue汽车租赁系统实战:车辆状态机与订单生命周期避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue汽车租赁系统实战:车辆状态机与订单生命周期避坑指南

简介:这是一套面向高校计算机相关专业学生与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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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>

版本号这里写的是我本地跑通的组合,你如果遇到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 service

baseURL 在开发环境用/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 + Redis,key 用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 方法,不用满项目搜数字。

最后说个我自己的习惯:每次改完订单状态相关的代码,一定手动跑一遍“下单→支付→取车→还车→完成”全流程,再跑一遍“下单→取消”和“下单→超时关闭”。汽车租赁系统的坑大多不在代码多难,而在状态流转的边界没覆盖全。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/1 5:53:27

eFuse+MCU协同:工业电源入口的过压过流反接保护设计

上个月有个客户返修板子&#xff0c;现场反馈就一句话&#xff1a;“设备突然就不开机了”。拆开外壳&#xff0c;电源入口那一小段铜皮已经烧成了黑色&#xff0c;板上主控芯片和一块DC-DC一起报废。查下来的原因其实很常见&#xff1a;现场操作人员把24V电源线正负极接反了&a…

作者头像 李华
网站建设 2026/10/23 18:04:01

用 LangSmith 打通 Agent 链路追踪:从 Trace 到回归测试的实战指南

上个月帮朋友排查他们内部客服机器人的 Bug&#xff0c;问题特别典型&#xff1a;用户提交退款申请&#xff0c;机器人明明查询到订单状态是“已退款”&#xff0c;最后却回复“退款正在处理中&#xff0c;请耐心等待”。从结果看这就是一次标准幻觉&#xff0c;但最让人头疼的…

作者头像 李华
网站建设 2026/10/23 20:05:16

dB与放大倍数转换全解析:从公式到运放增益设置实战

1. 从一次调试翻车说起&#xff1a;为什么dB和放大倍数总有人算错刚入行那会儿&#xff0c;我在调试一个音频采集前端&#xff0c;电路图上标着增益 40dB&#xff0c;我心想 40 倍嘛&#xff0c;直接把输入信号乘以 40 就完事了。结果实测输出波形削顶削得一塌糊涂&#xff0c;…

作者头像 李华
网站建设 2026/10/30 14:52:37

智能体工程化落地:从Demo到生产系统的关键实践

又到周五&#xff0c;惯例先把 GitHub Trending 扫一遍。这周排名靠前的仓库&#xff0c;跟三个月前完全不是一个画风。以前刷到的多是“XXX Agent Demo”“用LLM实现XX”这类一人演示型项目&#xff0c;现在站在榜单前排的是一批有部署文档、有可观测性设计、有业务案例的工程…

作者头像 李华
网站建设 2026/10/23 6:29:01

M.2 KEY A/B/E/M物理键位详解:识别、兼容与性能真相

1. M.2接口KEY A、B、E、M到底在说什么&#xff1f;别再被“插不进去”搞懵了 你拆开新买的主板&#xff0c;看到那个细长的插槽&#xff0c;旁边印着“M.2”三个字&#xff0c;旁边还有一行小字——KEY E。你手边刚好有一块标着“KEY BM”的固态硬盘&#xff0c;犹豫三秒&…

作者头像 李华
网站建设 2026/10/23 15:10:52

用AI告别周报焦虑:碎片记录+智能组装的实战指南

1. 周报焦虑的根源&#xff1a;你缺的不是工作量&#xff0c;是“翻译器” 周五下午六点&#xff0c;你盯着空白的周报文档&#xff0c;光标一闪一闪&#xff0c;像在嘲笑你。明明这一周开了七个会、回了上百条消息、改了三版方案、处理了两个线上问题&#xff0c;可手指放在键…

作者头像 李华