基于Vue与SpringBoot的汽车租赁系统设计与实现:从数据模型到JWT鉴权
简介这是一套基于Vue与SpringBoot的汽车租赁管理系统项目源码面向毕业设计、课程设计或期末大作业场景适合需要完整前后端分离项目参考的计算机专业学生与初级开发者。系统覆盖用户注册登录、车辆信息展示、在线预订、订单管理、用户反馈与后台管理等核心模块架构清晰能帮助理解Vue组件化开发和SpringBoot接口编写的完整流程。压缩包共包含655个文件大小31.47MBJava源码负责后端业务逻辑与数据库交互Vue文件提供前端页面组件svg图片用于图标素材xml和yml为项目配置sql脚本可直接导入数据库另含jar依赖、PPT演示文档及doc说明基本涵盖项目部署与展示所需的全部材料。同时保留run.bat、build.bat等快速启动脚本方便本地运行调试目前已有29人学习。无论是需要一套可答辩的完整项目还是想深入学习汽车租赁业务的具体实现都能从中获得直接参考。1. 租车系统用VueSpringBoot到底拆出了什么做汽车租赁管理系统作为毕设或课程设计最容易交出去的是“能用的页面”最容易被问到的是“订单状态怎么不失控”。我拿到这份基于Vue与SpringBoot的汽车租赁管理系统设计资源时先看了眼文件列表里面有 update-password.vue.bak、IndexAsideStatic.vue.bak、BreadCrumbs.vue.bak说明作者是边改边保留现场而 2-run.bat、3-build.bat 这类脚本又意味着它不只是一堆源码而是一套能本地跑起来的完整工程。对于刚开始接触前后端分离的开发者这套系统把用户注册登录、车辆信息展示、在线预订、订单管理、用户反馈和管理端等功能串了起来SpringBoot 负责业务逻辑与鉴权Vue 负责交互和界面。我会沿着数据模型、后端业务、前端联调讲到最后的一键部署和排错带着你把它拆开再装回去。2. 数据模型与权限边界租车系统为什么不能只建四张表2.1 核心模块拆分用户端与管理端不能共用一套接口先不要把“登录、车辆、订单、反馈”做成四个模块就完事。常见做法是把系统分成用户端和管理端两个上下文用户端关注车辆浏览、下单、退订、反馈管理端关注车辆上下架、订单审核、用户状态禁用。如果不把边界拆清楚后面就会出现普通用户把订单状态改成“已完成”的低级漏洞。我在设计时给 user 表加了角色字段 role取值 user/admin后端接口用拦截器统一判断角色而不是在每个 Controller 里重复校验。模块划分可以这样对应到页面用户注册与登录负责身份认证车辆信息展示提供列表和详情在线预订处理下单和车辆暂用锁定订单管理负责状态流转用户反馈生成评价记录系统管理维护基础数据。这六个模块就是摘要里提到的主线也是答辩时考官最关注的功能闭环。为了让表结构真正支撑起这些模块我在建表前会先把权限矩阵列出来用户端能改的只有自己的个人信息和可取消状态的订单管理端能改车辆状态、审核订单、禁用违规用户。后面所有 SQL 和接口设计都围绕这张矩阵展开。2.2 数据库表设计第三范式下的核心表和冗余取舍我一般用五张表起步user、car、rental_order、feedback、operation_log。operation_log 不算业务必需但排错时有很大价值。以下是简化建表 SQL省略了部分非关键字段CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT BCrypt哈希, phone VARCHAR(20), role TINYINT DEFAULT 0 COMMENT 0用户 1管理员, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE car ( id BIGINT AUTO_INCREMENT PRIMARY KEY, brand VARCHAR(30), model VARCHAR(50), plate_number VARCHAR(20) UNIQUE, price_per_day DECIMAL(10,2), status TINYINT DEFAULT 1 COMMENT 1可租 2已预订 3维修 4下架, location VARCHAR(100), update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE rental_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) UNIQUE, user_id BIGINT, car_id BIGINT, start_date DATE, end_date DATE, total_amount DECIMAL(10,2), status TINYINT DEFAULT 1 COMMENT 1待支付 2进行中 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );表设计的关键在 rental_order 里保留了 total_amount而不在查询时根据当日价格计算。这是因为车型调价后历史订单金额不能被篡改。类似地car 表中的 status 是冗余的可变状态但它必须与订单状态通过事务或状态机联动否则会出现“车辆明明已预订列表却显示可租”的脏数据。严格第三范式要求把关联拆开但这里刻意保留冗余是为了换取读取性能和业务闭环。实际执行时我还建了联合索引rental_order(user_id, status)、car(status, price_per_day)。前者用于“我的订单”按状态筛选后者用于首页车辆列表按状态和价格排序。如果都建单列索引MySQL 8.0 虽然可以用 index merge但代价更高。2.3 技术选型SpringBoot starters 和 Vue 组件化如何降低开发成本后端选 SpringBoot是因为 starter 机制把数据源、事务、Web 容器都内聚在依赖里不用像 SSM 那套自己拼一堆 XML。前端选 Vue.js是因为组件化让用户端和管理端能复用车辆卡片、订单状态标签这类界面。对比传统 JSP Servlet 方案这个组合在前后端分离后接口可独立测试页面也能并行开发。技术点SpringBoot VueJSP Servlet前后端分离天然支持强耦合状态管理Vuex/Pinia 路由守卫Session 串页面接口复用一个 API 多端调用重新渲染 JSP后续扩展可接小程序/App基本重写这个取舍也直接影响了运行时脚本2-run.bat 要同时拉起前端 Vite 后端 Tomcat后文会讲具体命令。选择这一组合的另一个原因是社区资料多无论是“Vue安装依赖”还是“SpringBoot配置”报错搜索时都能快速找到对应答案对课程设计和毕设来说这是隐形加分项。3. JWT鉴权与订单状态机SpringBoot 后端业务逻辑落地3.1 工程初始化pom.xml 别把版本调得太高我拆这个 zip 时发现后端依赖集中在 Web、Security、MyBatis-Plus、MySQL 和 JWT 上。用 IDEA 创建 SpringBoot 项目时Spring Initializr 默认给的版本往往是最新的这就会踩到“springboot版本太高”的坑。参考 pom.xml 片段如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency /dependenciesSpringBoot 版本太高时javax.servlet会被替换成jakarta.servlet如果照着旧教程写import javax.servlet.*项目直接起不来。我的建议是先固定某个自己熟悉的 2.7.x 或 3.x 小版本再按该版本找对应的 Security 配置方式。application.yml 里放 jwt.secret 和 token 过期时间数据库密码不要明文写死在 yml 中后文会给密文方案。3.2 用 JWT 代替 Session无状态登录怎么做汽车租赁系统里每个请求都要知道“我是谁、我是不是管理员”。用 Session 需要保存会话前后端分离后还要处理跨域 Cookie所以我选择 JWT。下面是一个简化版 JwtUtilpublic class JwtUtil { private static final SecretKey KEY Keys.secretKeyFor(SignatureAlgorithm.HS256); public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 60 * 60 * 1000)) .signWith(KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder().setSigningKey(KEY).build() .parseClaimsJws(token).getBody(); } }这里的 setSubject 存用户IDclaim 存角色。解析时如果 token 过期或签名不对会抛异常在全局异常处理器里统一返回 401。结合 Spring Security 的 OncePerRequestFilter把解析结果放入 SecurityContext后续 Controller 就能用AuthenticationPrincipal拿到身份。密码存储必须用 BCryptPasswordEncoder不要自己写哈希算法BCrypt 每次生成的盐不同能防止彩虹表攻击。3.3 订单状态机核心业务最容易出 Bug 的地方用户下单后订单要在“待支付、进行中、已取消、已完成”之间流转。如果不做状态机约束前端传一个 status 就能把订单改成任意值。我先定义状态迁移表当前状态允许流转到触发场景待支付进行中、已取消用户支付、超时取消进行中已完成到期还车已完成无订单结束已取消无订单关闭对应代码用枚举实现简洁且答辩容易讲public enum OrderStatus { WAIT_PAY(1, 待支付), RENTING(2, 进行中), FINISHED(3, 已完成), CANCELED(4, 已取消); private final int code; public boolean canTransferTo(int next) { if (this WAIT_PAY) return next RENTING || next CANCELED; if (this RENTING) return next FINISHED; return false; } }更新订单时我用条件更新而不是先查后改例如UPDATE rental_order SET status#{next} WHERE id#{id} AND status#{current}防止两个请求同时处理同一订单。还要在一个事务里同步 car 表下单把车辆状态改为“已预订”取消或完成再改回“可租”。这里的联动用 Spring 的Transactional保证原子性避免出现“订单待支付车辆却已锁定”的状态不一致。3.4 车辆查询接口分页、筛选和参数校验列表页要做品牌、价格区间、状态筛选接口不能写死 SQL。用 MyBatis-Plus 的 LambdaQueryWrapper 写条件构造GetMapping(/api/cars) public ResultPageCar list(RequestParam MapString, Object params) { PageCar page new Page(Long.parseLong(params.get(page)), Long.parseLong(params.get(size))); LambdaQueryWrapperCar wrapper new LambdaQueryWrapper(); wrapper.eq(params.containsKey(status), Car::getStatus, params.get(status)); wrapper.like(StrUtil.isNotBlank(params.get(brand)), Car::getBrand, params.get(brand)); wrapper.orderByAsc(Car::getPricePerDay); return carService.page(page, wrapper); }这里的 Map 参数接收 Query 字符串里的 page、size、status、brand。eq 和 like 的第一个参数是布尔条件能避免人为拼接字符串造成 SQL 注入。status 字段只接受 1、2、3、4 四个值我还会加一层枚举校验防止负数或 0 被传进来。分页参数也要做上限控制size 最多 50否则一次请求拉全表会把数据库打满。4. Vue组件化页面与路由联调前端实现与打包4.1 路由设计用户端、管理端和守卫前端工程拆开后有 main.js、IndexMain.vue、IndexHeader.vue、IndexAsideStatic.vue、BreadCrumbs.vue很明显是后台管理与用户端混合的单页应用。路由可以用懒加载区分模块const routes [ { path: /, component: () import(/views/Home.vue), meta: { requiresAuth: false } }, { path: /admin, component: () import(/layout/IndexMain.vue), meta: { requiresAuth: true, role: admin }, children: [ { path: car/list, component: () import(/views/admin/CarList.vue) }, { path: order/list, component: () import(/views/admin/OrderList.vue) } ] } ]vue 路由参数有两种传法query 在 URL 中用 ?id1params 用于动态段 path: /order/:id。我习惯在列表页跳详情页时用 query因为刷新页面参数还在params 若配合命名路由刷新后参数容易丢失。路由守卫里根据 meta.role 和后端返回的 userInfo 判断是否可访问所以要在 main.js 里通过router.beforeEach统一判断而不是在每个组件里写 if。4.2 Axios 封装统一 token、错误码和超时处理联调时最烦的是每个请求都写一遍 header。我在 request.js 里做了一层封装import axios from axios const service axios.create({ baseURL: /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( response response.data, error { if (error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这里设置 baseURL 为 /api再交给 vite 的 devServer 代理到后端。401 时统一跳登录页能避免业务组件各自处理 token 过期。要注意 response 拦截器返回的是 response.data而不是整个响应对象后续调用时才不会多包一层 data。如果后端返回的是{ code, message, data }结构我还会在response.data.code ! 0时统一弹错误提示这样前端业务代码只需要关心 data。4.3 备份文件与组件拆分update-password.vue.bak 怎么用项目正文里列出的update-password.vue.bak、IndexAsideStatic.vue.bak等文件是作者在调整组件时保留的副本。如果你拿到同名.bak只需要把后缀改回.vue再重启 vite 服务即可。这个做法对排查“改坏了但不知道哪步错”特别有用。组件边界我一般这样划IndexHeader 负责顶部用户信息IndexAsideStatic 负责菜单IndexMain 负责布局并嵌入 router-viewBreadCrumbs 根据当前路由生成面包屑。给一个最小化的布局示例template div classlayout IndexHeader / IndexAsideStatic / div classcontent BreadCrumbs / router-view / /div /div /template组件之间通过 props 传递数据全局状态如用户信息用 Pinia 管理不要用 event bus 到处转发。备份文件里如果看到 update-password.vue.bak大概率是作者在重写修改密码页面时留下的老版本你可以拿它和新文件 diff能看出哪些字段校验和请求方法发生了变化。4.4 打包后布局异常vue.config.js 里的坑前端开发完执行3-build.bat会调用构建命令。如果直接在 dist 目录打开 index.html会发现路由空白或 CSS 丢失这是因为资源路径默认是根路径/。如果你还停留在 Vue 安装及环境配置阶段先检查 node -v、npm -v确保依赖装完再执行npm run build。常见的修复方式是在 vue.config.js 里设置 publicPath 和代理module.exports { publicPath: process.env.NODE_ENV production ? /rent-car/ : /, devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }publicPath 决定了打包后所有静态资源的前缀。如果你的前端部署在 Nginx 的子目录不设对的话所有样式 404这也是遇到“vue 打包后布局异常”时最先要排查的点。路由使用 history 模式时还需要服务端配置 fallback否则刷新二级页面会 404如果不想动 Nginx 配置可以直接改用 hash 模式代价是 URL 上多个 #。5. 一键启动、备份恢复与安全加固技巧5.1 run.bat 和 3-build.bat 里藏着什么资源里的 2-run.bat 和 3-build.bat 是 Windows 批处理。我一般会把它们展开成两条命令理解echo off cd frontend start cmd /k npm run serve cd ..\backend start cmd /k mvn spring-boot:run2-run.bat 会同时打开两个终端分别启动 Vue 开发服务器和 SpringBoot。3-build.bat 则执行npm run build把前端 dist 产物复制到后端src/main/resources/static实现单 jar 部署。如果你要部署到多台服务器我更建议把 dist 放到 Nginx 下再用反向代理转发/api到后端这样升级互不干扰。5.2 配置文件密文和 heapdump 漏洞后端若把数据库密码直接写在 application.yml上传代码时很容易泄露。现在很多 SpringBoot 项目使用 jasypt-spring-boot-starter配置项写成 ENC(密文)启动时再解密。注意 jasypt 对 SpringBoot 版本有要求版本太高会出现解密失败所以引入时先查兼容矩阵。另一个安全点是 Spring Boot Actuator 的 heapdump 端点可能暴露 JVM 堆信息攻击者下载 heapdump 后能用工具分析内存中的密码和 token。最简单的做法是只暴露 healthmanagement: endpoints: web: exposure: include: health如果必须保留 info 或 metrics那就要对 actuator 路径做 IP 白名单或加 Spring Security 鉴权。这个漏洞在最近的安全巡检中经常被扫出来用作课程的加分项非常合适。5.3 手工验证用 curl 走一遍关键流程最后给出一个答辩时可演示的验证流程登录后取 token再以普通用户身份尝试调用管理员接口观察是否被拒绝。curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:test,password:123456} curl -X POST http://localhost:8080/api/order/update \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {orderId:1,status:3}第二条命令如果返回 403 或业务错误说明权限和状态机生效。实际演示时我会把第一条命令返回的 token 手动填进第二条的$TOKEN再配合抓包工具观察响应头里的 WWW-Authenticate 和状态码 401/403就能快速判断是 token 过期还是权限不足。这条验证路径比单纯点页面更有说服力也方便面试官当场检查你的逻辑边界。本文还有配套的精品资源点击获取