基于Vue与SpringBoot的汽车租赁系统设计与实现:从数据模型到JWT鉴权
2026/9/17 6:14:02 网站建设 项目流程

简介:这是一套基于Vue与SpringBoot的汽车租赁管理系统项目源码,面向毕业设计、课程设计或期末大作业场景,适合需要完整前后端分离项目参考的计算机专业学生与初级开发者。系统覆盖用户注册登录、车辆信息展示、在线预订、订单管理、用户反馈与后台管理等核心模块,架构清晰,能帮助理解Vue组件化开发和SpringBoot接口编写的完整流程。压缩包共包含655个文件,大小31.47MB:Java源码负责后端业务逻辑与数据库交互,Vue文件提供前端页面组件,svg图片用于图标素材,xml和yml为项目配置,sql脚本可直接导入数据库,另含jar依赖、PPT演示文档及doc说明,基本涵盖项目部署与展示所需的全部材料。同时保留run.bat、build.bat等快速启动脚本,方便本地运行调试,目前已有29人学习。无论是需要一套可答辩的完整项目,还是想深入学习汽车租赁业务的具体实现,都能从中获得直接参考。

1. 租车系统用Vue+SpringBoot到底拆出了什么?

做汽车租赁管理系统作为毕设或课程设计,最容易交出去的是“能用的页面”,最容易被问到的是“订单状态怎么不失控”。我拿到这份基于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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> </dependencies>

SpringBoot 版本太高时,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。下面是一个简化版 JwtUtil:

public 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 存用户ID,claim 存角色。解析时如果 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 ResultPage<Car> list(@RequestParam Map<String, Object> params) { Page<Car> page = new Page<>(Long.parseLong(params.get("page")), Long.parseLong(params.get("size"))); LambdaQueryWrapper<Car> 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 中用 ?id=1,params 用于动态段 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.bakIndexAsideStatic.vue.bak等文件,是作者在调整组件时保留的副本。如果你拿到同名.bak,只需要把后缀改回.vue,再重启 vite 服务即可。这个做法对排查“改坏了但不知道哪步错”特别有用。组件边界我一般这样划:IndexHeader 负责顶部用户信息,IndexAsideStatic 负责菜单,IndexMain 负责布局并嵌入 router-view,BreadCrumbs 根据当前路由生成面包屑。

给一个最小化的布局示例:

<template> <div class="layout"> <IndexHeader /> <IndexAsideStatic /> <div class="content"> <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:run"

2-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。最简单的做法是只暴露 health:

management: 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 过期还是权限不足。这条验证路径比单纯点页面更有说服力,也方便面试官当场检查你的逻辑边界。

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

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

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

立即咨询