ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot+Vue3汽车租赁系统全栈开发实战

Spring Boot+Vue3汽车租赁系统全栈开发实战 做汽车租赁系统这个选题的人这几年确实不少尤其是课程设计和毕业设计我接触到的绝大多数同学都挂在“技术栈太多不知道从哪里下手”这一关。标题里从 PHP、ASP.NET、Java 一路列到 Spring Boot、SSM、Vue3看起来是让你任选但恰恰因为选项太多很多人光搭环境就耗掉一周最后连一个能演示的主流程都跑不通。这篇文章就围绕这个选题讲清楚一件事如果你想在较短时间内做出一个功能完整、能正常演示、拿得出手的汽车租赁系统最稳妥的路径是什么。我会从技术选型、数据库设计、后端接口、前端页面到最终打包部署一条龙拆开讲代码和配置直接给到可直接复制改用的程度重点解释每个关键设计背后的原因而不是只丢一段“能跑就行”的代码。适合正在做这个课题的在校生也适合想用完整业务练手前后端分离开发的初学者。1. 技术方案选型与整体设计思路1.1 多个技术栈怎么选为什么 Java Vue3 是最大公约数这个选题本质上是一个典型的业务管理系统核心是“用户登录、车辆展示、下单租车、订单管理、后台维护”这一条完整链路。用 PHP、ASP.NET 或者 Java 都能做出来区别在于后续的扩展性、资料查找成本以及和你找工作时技术栈的匹配度。先说 PHP。PHP 在传统 Web 开发里确实很轻写个页面、查个数据库非常快几年以前的课设很多都是 PHP 一把梭。但现在的课程设计如果要求前后端分离PHP 这边的前端交互、接口鉴权、部署方式反而不如 Java 体系顺手。更现实的问题是你搜“PHP Vue 前后端分离”的教程质量参差不齐很多还是老式混编思路容易把新手绕晕。ASP.NET 同理技术在 Windows 系环境下没问题但如果你身边同学用的都是 Mac 或者统一 Linux 环境部署踩坑概率会明显增加。另外网上的资料总量和问题的命中率也明显低于 Java。Java Spring Boot Vue3 是这几年最“划算”的组合。Spring Boot 把 SSM 那套 Spring SpringMVC MyBatis 的配置复杂度降到很低MyBatis 依然是国内最主流的持久层框架Vue3 又是前端招聘里的常见要求。所以别贪多求全选这条主线。标题里写“SSM”也不用慌Spring Boot 项目里引入 MyBatis 就是现代化的 SSM 组合本质一样只是不再需要手动写一堆 XML 配置文件。1.2 系统模块划分前台、后台、服务端三层各管什么汽车租赁系统再怎么扩展核心也就三个使用方普通用户、业务管理员、系统管理员。课设阶段不需要把角色拆得太碎一般两级角色就够了普通用户操作前台页面管理员操作后台管理页。前台面向用户的功能包括注册登录个人资料维护浏览车辆列表按品牌、座位数、日租金等条件筛选查看车辆详情包括参数、图片、价格选择取车/还车日期创建租车订单在线支付押金与租金课设里模拟支付即可查看我的订单取消订单、确认还车后台面向管理员的功能包括车辆管理新增车辆、编辑、上下架、维护状态订单管理查看所有订单执行“确认出车”和“确认还车”等操作用户管理查看已注册用户禁用异常账号服务端就是一个 Spring Boot 应用提供 RESTful API前端用 axios 调接口完成数据交互。这样的分层结构既符合主流前后端分离的开发方式也方便在答辩时讲清楚“前端展示层—接口层—业务层—数据层”的完整架构。1.3 技术栈清单与版本踩坑预期我常用的组合是这样的后端JDK 8 或 11、Spring Boot 2.7.x、Maven、MyBatis、MySQL 8前端Vue3 Vite Element Plus Pinia Vue Router Axios权限方案JWT 拦截器开发调试前端 Vite 代理把 /api 转发到后端 8080为什么后端不建议一开始就上 Spring Boot 3.x因为 Spring Boot 3 把 javax 包名迁到了 jakarta很多老教程、老依赖不兼容新手遇到报错容易懵。先用 2.7.x 跑通主流程后期想折腾再升级。关于版本坑我在第 5 节会详细说。2. 数据库设计与核心业务逻辑2.1 三张核心表用户、车辆、订单汽车租赁的业务本质是“人对车辆的临时占用”所以最重要的表就三张用户表、车辆表、订单表。先把这三张表设计清楚系统就稳定了一半。用户表sys_userCREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), id_card VARCHAR(30), driver_no VARCHAR(30), role TINYINT DEFAULT 1, -- 1普通用户 2管理员 status TINYINT DEFAULT 1, -- 1正常 0禁用 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );车辆表carCREATE TABLE car ( id BIGINT PRIMARY KEY AUTO_INCREMENT, brand VARCHAR(50) NOT NULL, model VARCHAR(100) NOT NULL, plate_no VARCHAR(20) NOT NULL UNIQUE, daily_price DECIMAL(10,2) NOT NULL, deposit DECIMAL(10,2) NOT NULL, seat_count INT DEFAULT 5, gearbox VARCHAR(20), -- 自动挡/手动挡 energy_type VARCHAR(20), -- 燃油/新能源 cover_image VARCHAR(255), status TINYINT DEFAULT 0, -- 0空闲 1租赁中 2维修中 3已下架 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );订单表rental_orderCREATE TABLE rental_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, car_id BIGINT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, total_days INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0待支付 1待取车 2租赁中 3待还车 4已完成 5已取消 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里有两个细节值得注意。第一表名不要用 orderorder 是 SQL 里的关键字虽然加反引号也能用但后面写查询语句时到处都是反引号烦得很直接叫 rental_order 最省心。第二金额字段必须用 DECIMAL不要用 DOUBLE。租车金额涉及押金、租金、超时费用浮点计算容易出现 0.1 0.2 不等于 0.3 这种诡异现象后端一旦统一用 BigDecimal 就不会有这个问题。另外字段名我特意用下划线风格配合 MyBatis 的 map-underscore-to-camel-case 配置查询结果能自动映射成驼峰命名实体类后面写代码会非常顺。2.2 订单状态机从下单到还车的完整流转订单状态是整个系统最核心的流程任何一个环节断了演示就会出问题。我的设计里订单状态从 0 到 5流转方向大概是用户选车、选日期创建订单 → 状态 0待支付用户模拟支付成功后 → 状态 1待取车管理员在后台点击“确认出车” → 状态 2租赁中用户实际还车或管理员点击“确认还车” → 状态 3待还车管理员完成结算确认无额外费用 → 状态 4已完成用户在支付前主动取消或超时未支付 → 状态 5已取消状态流转看起来琐碎但它是后台订单管理页面的操作依据。管理员每个按钮对应一次状态变更比如“确认出车”按钮只在状态为 1 时显示“确认还车”按钮只在状态为 2 时显示。前端控制展示后端控制校验双重保障。价格计算的规则总金额 日租金 × 租车天数押金单独展示不包含在订单总额里。租车天数我用 ChronoUnit.DAYS.between(startDate, endDate) 计算。这里要注意用户在页面上选择的可能是日期而不是“天数”那么按自然日算会比按 24 小时算更直观。比如 1 月 1 日取车、1 月 3 日还车租车天数是 2 天而不是 48 小时。这样既方便计算也符合租车行业“按天计费”的惯例。2.3 车辆时间冲突校验避免同一辆车被重复预订这是整个系统里最值得拿出来讲的业务点也经常是答辩时老师会问的“你的系统有没有考虑并发”的地方。其实逻辑很简单和酒店订房重叠判断一模一样两个时间段 [start1, end1] 和 [start2, end2] 存在重叠当且仅当 start1 end2 AND start2 end1。对应到租车场景用户要租某辆车如果存在一个状态不是已取消的订单它的 startDate 和 endDate 与用户选择的日期段重叠就说明这辆车在这个时间段已经被占了。用 MyBatis 查询时SQL 可以这样写SELECT COUNT(*) FROM rental_order WHERE car_id #{carId} AND status NOT IN (5) AND start_date #{endDate} AND end_date #{startDate}count 大于 0 就直接提示用户“该车辆在本时段已被预订请更换日期或选择其他车辆”。这个判断放在创建订单的 Service 层里采用数据库查询判断的方式对课设级别完全够用。如果真要做到高并发防重那就要对车辆加分布式锁或者唯一约束不是这类项目该纠结的点。3. 后端实现Spring Boot SSM 的分层落地3.1 项目初始化与依赖配置创建 Spring Boot 项目建议直接用 Spring Initializr 网页端生成选择 Maven、Java 8、Spring Boot 2.7.x依赖里选 Spring Web 和 MySQL Driver 就行MyBatis 和 JWT 手动加。我常用的 pom.xml 核心依赖是这样的dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyHutool 是个好东西后面生成订单号、做 BCrypt 密码加密、写日期工具类都用得上省掉不少重复代码。引入了这堆依赖之后application.yml 里把数据源和 MyBatis 配置好spring: datasource: url: jdbc:mysql://localhost:3306/car_rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mapper/*.xml这里 map-underscore-to-camel-case 必须开否则你数据库里查出来的 start_date 映射不到 startDate 实体字段上接口返回的全是 null排查起来特别迷惑。3.2 Controller、Service、Mapper 三层的标准写法后端代码结构我习惯分成 controller、service、mapper、entity、common 五个包。common 里放 Result、JWT 工具类、全局异常处理这些公共部分。以创建订单这个核心接口为例Controller 只负责接收参数和返回结果PostMapping(/order/create) public Result createOrder(RequestBody OrderCreateDTO dto, RequestHeader(Authorization) String token) { Long userId JwtUtil.getUserId(token); return Result.ok(orderService.createOrder(userId, dto)); }Service 里完成核心校验和业务逻辑Transactional public Order createOrder(Long userId, OrderCreateDTO dto) { Car car carMapper.selectById(dto.getCarId()); if (car null || car.getStatus() ! 0) { throw new BusinessException(车辆不存在或不可租用); } int days (int) ChronoUnit.DAYS.between(dto.getStartDate(), dto.getEndDate()); if (days 0) { throw new BusinessException(还车日期必须晚于取车日期); } int count orderMapper.countOverlap(dto.getCarId(), dto.getStartDate(), dto.getEndDate()); if (count 0) { throw new BusinessException(该车辆在本时段已被预订); } Order order new Order(); order.setOrderNo(R System.currentTimeMillis()); order.setUserId(userId); order.setCarId(dto.getCarId()); order.setStartDate(dto.getStartDate()); order.setEndDate(dto.getEndDate()); order.setTotalDays(days); order.setTotalAmount(car.getDailyPrice().multiply(BigDecimal.valueOf(days))); order.setStatus(0); orderMapper.insert(order); return order; }这里有三层校验车辆必须存在且空闲、日期必须合法、时间段不能冲突。校验顺序也有讲究先查车辆再算天数再查冲突避免用户随意提交非法数据打到数据库。加 Transactional 保证事务安全万一后面插入失败不会留下脏数据。Mapper 接口配合 XML 或注解都能做。如果查询不复杂MyBatis 注解其实够用比如Select(SELECT COUNT(*) FROM rental_order WHERE car_id #{carId} AND status NOT IN (5) AND start_date #{endDate} AND end_date #{startDate}) int countOverlap(Param(carId) Long carId, Param(startDate) LocalDate startDate, Param(endDate) LocalDate endDate);3.3 JWT 登录鉴权与全局异常处理用户和管理员登录成功后后端生成一个 JWT 返回给前端。JWT 里只放 userId 和 role 两个关键信息过期时间设成 24 小时。用 java-jwt 生成Algorithm algorithm Algorithm.HMAC256(your-secret-key); String token JWT.create() .withClaim(userId, user.getId()) .withClaim(role, user.getRole()) .withExpiresAt(new Date(System.currentTimeMillis() 86400000)) .sign(algorithm);再写一个拦截器拦截所有 /api/** 请求但放行登录、注册、车辆列表这些公开接口。注意拦截器里拿到 token 后要解析 userId 放回 request 的 attribute 里方便 Controller 直接取避免每个接口都重复解析一次。全局异常处理用一个 RestControllerAdvice 兜底业务异常返回 Result 里的错误码和提示其他异常返回“系统繁忙”的通用信息。这一步很值得做因为前端拿到统一结构的错误提示才能直接弹消息框给用户看。不做的后果就是接口一报错前端控制台一堆英文堆栈演示当场翻车。4. 前端实现Vue3 页面搭建与联调部署4.1 Vue3 前端工程结构设计前端用 Vite 创建工程npm create vuelatest模板选择时把 Vue Router、Pinia 选上工具链部分选 ESlint 和 Prettier 按个人习惯即可。依赖安装npm install element-plus axios页面结构按角色分开。普通用户能看到首页、车辆列表、车辆详情、创建订单、我的订单、登录注册这些页面管理员进入后台路由包括车辆管理、订单管理、用户管理。路由用懒加载方式引入组件避免首屏一次性加载太多内容。Element Plus 在这个项目里非常好用。车辆列表直接用 el-card 做卡片式展示后台表单用 el-form 加校验订单表格用 el-table 配 el-tag 显示状态整体出效果很快不需要写太多手搓样式。4.2 Axios 封装与跨域代理的两种姿势前端所有接口请求统一走 axios 实例封装时最重要的两件事是请求拦截器注入 token响应拦截器统一处理 401 和业务错误。const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { if (response.data.code ! 200) { ElMessage.error(response.data.message) return Promise.reject(new Error(response.data.message)) } return response.data }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )开发环境跨域问题我强烈推荐用 Vite 代理而不是后端开 CORS。后端开 CORS 容易在拦截器顺序、预检请求上踩坑而代理是在开发服务器层面转发请求浏览器看到的是同源请求干净利落// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }后端接口统一以 /api 开头Controller 层直接写 RequestMapping(/api/car) 也行或者用 server.servlet.context-path/api 统一加前缀。两种方式选一个就行不要同时用否则路径变两遍。4.3 打包把 Vue 构建产物装进 Spring Boot开发完成后前端要打包给后端统一部署。最省事的方式是执行 npm run build把生成的 dist 目录下的文件复制到 Spring Boot 项目的 src/main/resources/static 里然后重新打包后端 jar。启动后浏览器直接访问 http://localhost:8080 就能看到前端页面接口也走同源不存在跨域问题。如果路由用了 history 模式要注意一个问题直接访问 /car/1 刷新页面会返回 404因为 Spring Boot 找不到对应的 Controller。解决办法有两个。一个是图省事把 Vue Router 改成 hash 模式URL 会带上 # 号不美观但绝对稳定。另一个是在后端加一个转发 Controller把非 /api 开头的路径都转发到 index.htmlController public class PageForwardController { RequestMapping(value {/, /car/**, /order/**, /admin/**}) public String forward() { return forward:/index.html; } }我个人建议课设阶段直接用 hash 模式省一个类也少一个配置点。如果追求最终效果好看再用 history 模式加转发。5. 常见问题与排查实录5.1 开发环境与依赖版本类问题这类问题出现的频率最高而且很多并不是代码逻辑错了纯粹是环境不一致导致。问题现象大概率原因解决建议Spring Boot 3 项目启动报 class not found jakarta.* 相关错误依赖引入了 javax 老版本库低版本 JDK 起步或者统一换 Spring Boot 3 兼容版本MyBatis 查询结果全是 null没开启驼峰映射application.yml 配置 map-underscore-to-camel-case: true前端请求接口报跨域前后端端口不同且未代理优先用 Vite proxy少用后端 CrossOriginMaven 依赖下载慢或失败网络问题配置阿里云镜像仓库Vue 页面组件能显示但接口 404请求路径和后端 Controller 不一致前后端统一约定 /api 前缀检查 RequestMapping热词里有一条“springboot版本太高”说的就是这个老老实实先选 2.7.x不要盲目追最新。5.2 业务逻辑和数据类问题订单时间冲突判断是我见过最容易被忽略的地方。很多人只判断车辆当前是否空闲却不管用户选的时间段和已有订单是否重叠结果就是两个人订了同一辆车。上面给出的 countOverlap SQL 就是解决这个问题的标准写法。日期参数另一个坑是前后端格式不一致。前端传 2026-05-01后端用 LocalDate 接收时Spring 默认可能解析不了。在 application.yml 里加一段配置spring: jackson: date-format: yyyy-MM-dd time-zone: GMT8如果接口还接收时间字段可以给实体字段加 JsonFormat(pattern yyyy-MM-dd)双保险。密码存储也要注意。明文存密码是答辩时最容易被老师挑刺的点用 Hutool 的 BCrypt 加密注册时加密登录时校验代码只有两行String hashed BCrypt.hashpw(rawPassword); boolean check BCrypt.checkpw(rawPassword, hashed);5.3 部署演示阶段的顺手技巧图片上传和展示是最常见的“平时好好的演示时挂了”类问题。车辆图片如果保存在本地磁盘目录Spring Boot 默认不会把它映射成可访问的静态资源。需要在配置类里加一个资源映射Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.base-dir}) private String baseDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: baseDir /); } }路径分隔符记得不要写死成 Windows 的反斜杠用 Paths.get 或者直接配置里写相对路径否则部署到云服务器就炸。还有一个小技巧给的数据库脚本里一定要预置测试数据。建议至少放一个 admin 管理员账号、五个不同品牌车辆、一个已完成的历史订单和一个进行中的订单。这样打开系统就能直接演示前后台全部流程不用现场注册、现场造数据。6. 我的一些实际操作心得把整条链路从头到尾跑过一遍之后我的感受是这个选题真正的难点不在某个技术本身而在于你要把“租赁业务的时间冲突校验”“订单状态流转的闭环”“前端操作与后端状态的联动”这三件事同时想清楚。很多同学进度卡住不是因为不会写 CRUD而是没想明白车辆凭什么不能被重复预订、状态按钮凭什么在不同阶段显示不同内容这一类业务规则导致代码写了一半推倒重来。另一个值得养成的习惯是从一开始就用统一返回结构。Result{ code, message, data } 这套东西所有接口都按这个格式返回前端 axios 响应拦截器统一处理后面联调时你会省下大量时间。千万别这个接口返回对象、那个接口直接返回字符串联调时前端光写类型判断就够烦了。如果后续想再扩展还可以给这个系统加一个“超时还车自动计费”的逻辑还车日期超过 endDate 时按日租金的 1.5 倍补收超期费用。资金有限又不能出错的话我的建议就是照着本文这个顺序走先数据库后代码先后端后前端先主流程后细节。主流程跑通之后其他都是锦上添花。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进