Spring Boot+Vue汽车租赁系统实战:状态机与租期校验
2026/9/16 14:13:10 网站建设 项目流程

简介:这是一套面向计算机专业本科生及Java初学者的毕业设计级实战项目,聚焦汽车租赁业务场景,完整呈现前后端分离架构下的系统开发全流程。资源适用于课程设计、期末大作业及毕设参考,帮助学习者掌握Spring Boot后端开发、Vue.js前端构建、MySQL数据库设计与项目整体部署等核心技能。压缩包为ZIP格式,大小15.06MB,包含经严格调试可直接运行的完整源码、建库建表SQL脚本、开发说明文档、部署操作视频与关键代码讲解视频,以及配套开发环境软件,覆盖从环境搭建到功能验证的全链路支持。目前已有77人下载学习,所有模块均经导师指导并高分通过验收,目录结构规范,前后端代码职责清晰,注释充分,特别适合缺乏项目经验的学习者快速上手、理解企业级应用分层设计逻辑与协同开发模式。

1. 这不是又一个“学生毕设模板”:Spring Boot + Vue 汽车租赁系统的真实落地价值在哪?

很多人看到“Java项目-基于SpringBoot+Vue的汽车租赁系统”第一反应是:毕业设计、课程作业、源码商城里的泛泛之作。但实际拆开这套组合——Spring Boot 3.x 的自动装配与分层治理能力、Vue 3 的 Composition API 与响应式系统、MySQL 8 的事务隔离与外键约束、以及汽车租赁场景中特有的「车辆状态流转」「租期重叠校验」「押金冻结解冻」逻辑——它恰恰是中小型企业快速验证业务模型的最小可行架构(MVP)。这套系统能跑通从用户注册、车辆检索、预约下单、合同生成、支付回调到归还结算的全链路,且所有模块都可独立演进:比如把 Vue 前端替换成 uni-app 接入小程序,或把 MySQL 换成 PostgreSQL 支持地理围栏查询。它不追求炫技,但每个环节都踩在 Java Web 开发的主流技术栈上:MyBatis-Plus 的条件构造器、Spring Security 的 RBAC 权限控制、Vue Router 的路由守卫、Axios 的请求拦截器。适合刚完成 Spring Boot 基础学习、想用真实业务串联知识点的开发者,也适合需要快速交付轻量级 SaaS 功能的技术负责人评估技术选型边界。

2. 后端核心:用 Spring Boot 3.2 + MyBatis-Plus 实现车辆状态机与租期冲突检测

汽车租赁系统的业务复杂度不在界面交互,而在后端对“时间”和“状态”的双重约束。一辆车在同一时间段内不能被两个订单占用,而车辆本身又有“待租”“已租出”“维修中”“已报废”等状态跃迁。Spring Boot 3.2 提供了更严格的 Jakarta EE 9+ 规范支持,配合 MyBatis-Plus 3.5.4 的 LambdaQueryWrapper,能写出可读性强、类型安全的业务逻辑。

2.1 车辆状态流转建模与数据库设计

车辆状态不能靠前端传参硬编码控制,必须由后端根据当前状态和操作动作进行合法性校验。我们定义CarStatusEnum枚举:

public enum CarStatusEnum { AVAILABLE("待租", 1), RENTED("已租出", 2), MAINTAINING("维修中", 3), SCRAPPED("已报废", 4); private final String desc; private final int code; CarStatusEnum(String desc, int code) { this.desc = desc; this.code = code; } // getter 省略 }

对应car表中status字段为TINYINT类型,避免字符串比较开销。关键在于rentreturnCar方法中嵌入状态机判断:

@Transactional public Result rentCar(Long carId, Long userId, LocalDateTime startTime, LocalDateTime endTime) { Car car = carMapper.selectById(carId); if (!CarStatusEnum.AVAILABLE.getCode().equals(car.getStatus())) { return Result.fail("车辆不可租用,当前状态:" + CarStatusEnum.fromCode(car.getStatus()).getDesc()); } // 校验租期是否与其他订单重叠(核心防重逻辑) int conflictCount = orderMapper.selectCount( new LambdaQueryWrapper<Order>() .eq(Order::getCarId, carId) .ne(Order::getStatus, OrderStatusEnum.CANCELLED.getCode()) .apply("NOT (end_time <= ? OR start_time >= ?)", startTime, startTime) ); if (conflictCount > 0) { return Result.fail("该车辆在指定时段内已被预约"); } // 执行租用:更新车辆状态 + 创建订单 car.setStatus(CarStatusEnum.RENTED.getCode()); carMapper.updateById(car); Order order = new Order(); order.setCarId(carId); order.setUserId(userId); order.setStartTime(startTime); order.setEndTime(endTime); order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode()); orderMapper.insert(order); return Result.success(order.getId()); }

注意apply("NOT (end_time <= ? OR start_time >= ?)")是时间重叠的经典判式,等价于(start_time < ? AND end_time > ?),但用NOT (A OR B)写法在 MySQL 中更容易命中索引。务必在order(car_id, start_time, end_time)上建立联合索引。

2.2 使用 Spring Security 实现细粒度权限控制

汽车租赁系统需区分普通用户、客服、管理员三类角色。Spring Boot 3.2 默认使用 Jakarta EE 的jakarta.servlet.http.HttpServletRequest,Security 配置需适配新包路径:

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/user/**").authenticated() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/customer-service/**").hasAnyRole("ADMIN", "CUSTOMER_SERVICE") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard") .failureUrl("/login?error=true") ) .logout(logout -> logout .logoutSuccessUrl("/login?logout=true") ); return http.build(); } }

角色数据存于sys_user_role关联表,UserDetailsServiceImpl中加载权限时,将role_code(如ROLE_ADMIN)映射为GrantedAuthority。Vue 前端通过/api/user/info获取roles数组,动态控制按钮显隐。

2.3 数据库脚本关键约束与索引优化

提供的database.sql不应只是建表语句堆砌,必须包含业务强约束:

-- 车辆表:添加唯一索引防止重复车牌 CREATE TABLE `car` ( `id` bigint NOT NULL AUTO_INCREMENT, `plate_number` varchar(20) NOT NULL COMMENT '车牌号', `brand` varchar(50) NOT NULL, `model` varchar(50) NOT NULL, `status` tinyint NOT NULL DEFAULT '1' COMMENT '1-待租,2-已租出,3-维修中,4-已报废', PRIMARY KEY (`id`), UNIQUE KEY `uk_plate_number` (`plate_number`) -- 强制唯一 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- 订单表:联合索引支撑租期冲突查询 CREATE TABLE `order` ( `id` bigint NOT NULL AUTO_INCREMENT, `car_id` bigint NOT NULL, `user_id` bigint NOT NULL, `start_time` datetime NOT NULL, `end_time` datetime NOT NULL, `status` tinyint NOT NULL DEFAULT '1', PRIMARY KEY (`id`), KEY `idx_car_time` (`car_id`,`start_time`,`end_time`) -- 关键!用于重叠查询 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

提示idx_car_time索引顺序必须是(car_id, start_time, end_time),因为查询条件中car_id是等值匹配,start_time/end_time是范围查询,遵循最左前缀原则。若顺序颠倒,该索引无法用于WHERE car_id = ? AND ...场景。

3. 前端工程:Vue 3 + Pinia + Element Plus 构建可维护的租赁业务视图

Vue 部分不是简单套用 Admin 模板,而是围绕“租车流程”组织组件结构:车辆列表页需支持按品牌、价格区间、座位数筛选;订单页需展示时间轴(预约→支付→取车→还车);用户中心要聚合历史订单、待处理订单、押金余额。Vue 3 的<script setup>语法配合 Pinia 状态管理,让业务逻辑与 UI 解耦。

3.1 使用 Composition API 封装车辆搜索逻辑

车辆检索涉及多条件组合(品牌模糊匹配、价格区间、状态筛选),直接在组件内写watchcomputed易导致逻辑分散。封装为自定义 Hook:

<!-- src/composables/useCarSearch.js --> import { ref, watch } from 'vue' import { searchCars } from '@/api/car' export function useCarSearch() { const searchParams = ref({ brand: '', minPrice: null, maxPrice: null, status: null // 1-待租, 2-已租出... }) const carList = ref([]) const loading = ref(false) const loadCars = async () => { loading.value = true try { const res = await searchCars(searchParams.value) carList.value = res.data } finally { loading.value = false } } // 监听参数变化自动触发搜索 watch(searchParams, loadCars, { deep: true, immediate: true }) return { searchParams, carList, loading, loadCars } }

CarList.vue中调用:

<script setup> import { useCarSearch } from '@/composables/useCarSearch' const { searchParams, carList, loading } = useCarSearch() // 可直接绑定到表单元素 </script> <template> <el-input v-model="searchParams.brand" placeholder="输入品牌关键词" /> <el-slider v-model="searchParams.minPrice" :max="1000" /> <el-button @click="loadCars">搜索</el-button> <el-table :data="carList" :loading="loading"> <!-- 表格列 --> </el-table> </template>

3.2 Pinia store 管理全局用户状态与订单缓存

用户登录态、未读消息数、当前选中车辆 ID 等跨组件状态,不应存在多个组件的ref中。Pinia store 统一管理:

// src/stores/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: null, unreadCount: 0, selectedCarId: null // 用于从列表页跳转到预订页时传递ID }), getters: { isLoggedIn: (state) => !!state.token }, actions: { setToken(token) { this.token = token localStorage.setItem('token', token) }, clearToken() { this.token = '' localStorage.removeItem('token') this.userInfo = null }, updateUnread(count) { this.unreadCount = count } } })

main.js中挂载后,任意组件可通过const userStore = useUserStore()访问。例如在导航栏中显示未读消息数:

<el-badge :value="userStore.unreadCount" :max="99"> <el-icon><Bell /></el-icon> </el-badge>

3.3 Axios 请求拦截与错误统一处理

汽车租赁系统涉及大量异步请求(获取车辆、提交订单、支付回调),需统一处理 token 注入、Loading 状态、错误码跳转:

// src/utils/request.js import axios from 'axios' import { ElMessage, ElLoading } from 'element-plus' import { useUserStore } from '@/stores/user' const service = axios.create({ baseURL: '/api', timeout: 10000 }) let loadingInstance = null service.interceptors.request.use( config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } // 对于非登录页的请求显示 loading if (config.url !== '/auth/login') { loadingInstance = ElLoading.service({ fullscreen: true }) } return config }, error => { if (loadingInstance) loadingInstance.close() return Promise.reject(error) } ) service.interceptors.response.use( response => { loadingInstance?.close() const { code, msg, data } = response.data if (code === 200) { return data } else if (code === 401) { // token 过期,清空状态并跳转登录 const userStore = useUserStore() userStore.clearToken() window.location.href = '/login' return Promise.reject(new Error('登录已过期')) } else { ElMessage.error(msg || '请求失败') return Promise.reject(new Error(msg)) } }, error => { loadingInstance?.close() ElMessage.error('网络异常,请检查网络连接') return Promise.reject(error) } ) export default service

4. 全链路部署:Nginx 反向代理 + Spring Boot JAR 包 + MySQL 容器化运行

一套能跑起来的系统,部署环节常被忽略。但生产环境要求前后端分离部署、静态资源 CDN 化、API 接口反向代理、数据库连接池调优。这里给出基于 Linux 服务器(CentOS 7+/Ubuntu 22.04)的最小可行部署方案,不依赖 Docker Compose 编排,直击运维本质。

4.1 后端 JAR 包启动与 JVM 参数调优

Spring Boot 打包为car-rental-backend-1.0.jar后,不应直接java -jar运行。需配置 JVM 参数防止 OOM,并用 systemd 管理进程生命周期:

# 创建 /etc/systemd/system/car-rental.service [Unit] Description=Car Rental Backend Service After=network.target [Service] Type=simple User=appuser WorkingDirectory=/opt/car-rental ExecStart=/usr/bin/java -Xms512m -Xmx1024m -XX:+UseG1GC -Dfile.encoding=UTF-8 -jar /opt/car-rental/car-rental-backend-1.0.jar --spring.profiles.active=prod Restart=always RestartSec=10 Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 [Install] WantedBy=multi-user.target

关键参数说明:

  • -Xms512m -Xmx1024m:初始堆内存 512MB,最大 1024MB,避免频繁 GC
  • -XX:+UseG1GC:启用 G1 垃圾收集器,适合大堆内存与低延迟场景
  • --spring.profiles.active=prod:激活生产配置,读取application-prod.yml

启用服务:

sudo systemctl daemon-reload sudo systemctl enable car-rental.service sudo systemctl start car-rental.service sudo systemctl status car-rental.service # 查看运行状态

4.2 Nginx 配置反向代理与静态资源托管

Vue 打包后的dist目录需由 Nginx 托管,同时将/api路径代理至后端服务:

# /etc/nginx/conf.d/car-rental.conf upstream backend { server 127.0.0.1:8080; # Spring Boot 默认端口 } server { listen 80; server_name rental.yourdomain.com; # 前端静态资源 location / { root /opt/car-rental/dist; try_files $uri $uri/ /index.html; } # API 接口代理 location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 30s; } # 静态资源缓存(CSS/JS/IMG) location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } }

重启 Nginx 并验证:

sudo nginx -t # 测试配置 sudo systemctl restart nginx curl -I http://rental.yourdomain.com/api/user/info # 应返回 200

4.3 MySQL 生产配置与连接池参数校准

application-prod.yml中的数据库配置不能照搬开发环境。HikariCP 连接池需根据服务器 CPU 核数与并发预期调整:

spring: datasource: url: jdbc:mysql://localhost:3306/car_rental?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: rental_app password: your_secure_password driver-class-name: com.mysql.cj.jdbc.Driver sql: init: mode: never # 生产环境禁用自动建表,由 DBA 统一管理 # HikariCP 连接池调优(参考:4核8G服务器) hikari: maximum-pool-size: 20 # 最大连接数 = 2 × CPU 核数(常见经验值) minimum-idle: 5 # 最小空闲连接,避免频繁创建销毁 connection-timeout: 30000 # 连接超时 30s idle-timeout: 600000 # 空闲连接最大存活时间 10 分钟 max-lifetime: 1800000 # 连接最大生命周期 30 分钟(避免 MySQL wait_timeout 断连) validation-timeout: 3000 # 连接有效性校验超时 connection-test-query: SELECT 1 # 连接校验 SQL

提示:MySQL 服务端需同步调整wait_timeout(默认 28800 秒 = 8 小时),建议设为1800(30 分钟),与max-lifetime对齐,避免连接池持有已失效连接。

5. 源码级调试技巧:快速定位“车辆租不出去”与“订单状态不更新”的根因

拿到源码后,新手常卡在“功能看似正常,但实际业务走不通”。比如点击“立即租赁”按钮无反应,或支付成功后订单状态仍是“待支付”。这类问题不靠猜,而靠三步定位法:日志追踪 → SQL 验证 → 状态机断点。

5.1 日志分级与关键埋点位置

Spring Boot 默认日志级别为INFO,但排查状态流转问题需DEBUG级别。在application-prod.yml中开启 MyBatis 日志:

logging: level: com.example.rental.mapper: DEBUG # 查看 SQL 执行 com.example.rental.service: DEBUG # 查看业务方法入参与返回 org.springframework.security: WARN # 降低 Security 日志噪音

重点关注rentCar()方法开头打印入参、中间打印冲突查询 SQL、结尾打印更新结果。若日志中出现selectCount返回0但页面仍提示“已被预约”,说明前端传入的startTime/endTime格式有误(如时区未转换为 UTC)。

5.2 数据库脚本执行验证与约束检查

源码附带的database.sql必须逐条验证,尤其检查外键与索引是否生效:

-- 登录 MySQL,检查 car 表是否有 uk_plate_number 索引 SHOW INDEX FROM car WHERE Key_name = 'uk_plate_number'; -- 检查 order 表 idx_car_time 是否存在且字段顺序正确 SHOW INDEX FROM `order` WHERE Key_name = 'idx_car_time'; -- 验证外键约束(如 order.car_id 关联 car.id) SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'car_rental' AND TABLE_NAME = 'order' AND REFERENCED_TABLE_NAME IS NOT NULL;

REFERENCED_TABLE_NAME为空,说明外键未创建成功,需检查 SQL 中FOREIGN KEY语法是否符合 MySQL 8.0+ 规范(如引擎必须为 InnoDB)。

5.3 Vue 路由守卫与 Axios 拦截器联合调试

前端状态不更新,常因接口调用失败但未被捕获。在src/utils/request.js的响应拦截器中加断点:

service.interceptors.response.use( response => { console.log('[API SUCCESS]', response.config.url, response.data) // 加此行 // ...原有逻辑 }, error => { console.error('[API ERROR]', error.config?.url, error.response?.status, error.message) // 加此行 // ...原有逻辑 } )

打开浏览器 DevTools → Console,点击“立即租赁”按钮,观察是否打印[API ERROR] /api/order/rent 400 ...。若出现400 Bad Request,说明后端校验失败,此时查看 Network 面板中该请求的 Payload,确认startTime是否为 ISO 格式(2024-06-15T08:00:00),而非本地时间字符串(2024-06-15 08:00:00)。

技巧:在 Vue 组件中,使用dayjs().toISOString()生成标准时间戳,而非new Date().toLocaleString()。后者受浏览器时区影响,易导致后端解析失败。

最后,在rentCar()方法首行加断点,用 IDE(IntelliJ IDEA)远程调试,观察LambdaQueryWrapper构造的 SQL 是否与预期一致,conflictCount计算逻辑是否覆盖所有重叠场景(如订单 A 的start_time=10:00,end_time=12:00,订单 B 的start_time=11:00,end_time=13:00,必须被检测出)。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询