1. 先搞清楚“汽车租赁系统”到底要做什么以及为什么选 Spring Boot Vue3一提到“汽车租赁系统”很多人的第一反应是这不就是一个简单的增删改查CRUD项目吗确实它的核心业务逻辑——车辆管理、客户管理、订单管理、费用结算——听起来并不复杂。但如果你真的打算动手做一个无论是为了学习、毕业设计还是作为一个小型商业应用的起点你会发现难点从来不在于“做什么”而在于“怎么做”才能让项目跑得稳、扩展性好、前后端协作顺畅。这就是为什么 Spring Boot Vue3 这个技术栈组合会成为当前开发这类管理系统的热门选择。Spring Boot 让你能快速搭建一个稳定、易于维护的后端 API 服务而 Vue3 的组合式 API 和响应式系统则让前端界面的开发和状态管理变得前所未有的清晰。这个组合解决的核心问题是如何高效、清晰地分离前后端职责并让它们能独立开发、独立部署最终又能无缝集成。所以这篇文章不是给你一个“玩具级”的 Demo而是带你从零开始搭建一个具备完整前后端分离架构的汽车租赁系统原型。我会重点讲清楚几个关键点项目结构如何设计前后端代码放一起还是分开开发时怎么联调核心业务表如何设计车辆、客户、订单、费用它们之间的关系是什么前后端如何通信API 接口怎么设计跨域问题怎么解决开发与部署的差异开发时用 Vite 热更新打包后如何集成到 Spring Boot 里如果你正在学习 Spring Boot 或 Vue3想找一个有完整业务场景的项目来练手或者你需要快速构建一个管理系统原型那么跟着这个思路走能帮你避开很多初次整合时会遇到的坑。2. 环境准备与项目骨架搭建告别混乱的目录结构动手之前先把环境理清楚。我建议将前后端作为两个独立的项目来管理这样更符合现代开发的习惯也便于后续的维护和部署。2.1 开发环境清单确保你的机器上已经安装了以下工具JDK 17 或更高版本Spring Boot 3.x 推荐使用 JDK 17。用java -version检查。Maven 3.6用于管理 Spring Boot 项目的依赖和构建。用mvn -v检查。Node.js 18 和 npm/yarnVue3 的运行环境。用node -v和npm -v检查。一个 IDE 或编辑器后端推荐 IntelliJ IDEA 或 VS Code安装 Spring Boot 扩展包前端推荐 VS Code。数据库选择 MySQL 8.0 或 PostgreSQL。本文以 MySQL 为例。2.2 创建独立的后端 Spring Boot 项目使用 Spring Initializr 或 IDE 的创建向导生成一个项目。关键依赖选择Spring Web用于构建 RESTful API。Spring Data JPA简化数据库操作。MySQL Driver数据库连接驱动。Lombok选装用于简化实体类代码。生成后的项目结构大致如下car-rental-backend/ ├── src/ │ ├── main/ │ │ ├── java/com/example/carrental/ │ │ │ ├── CarRentalApplication.java // 启动类 │ │ │ ├── controller/ // 控制器接收HTTP请求 │ │ │ ├── entity/ // 实体类对应数据库表 │ │ │ ├── repository/ // 数据访问层接口 │ │ │ ├── service/ // 业务逻辑层 │ │ │ └── dto/ // 数据传输对象 │ │ └── resources/ │ │ ├── application.properties // 配置文件 │ │ └── static/ // 后期放前端打包文件 └── pom.xml在application.properties中配置数据库和端口# 应用端口避免和前端开发服务器冲突 server.port8080 # 数据库配置 spring.datasource.urljdbc:mysql://localhost:3306/car_rental_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.passwordyourpassword spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver # JPA 配置 spring.jpa.hibernate.ddl-autoupdate spring.jpa.show-sqltrue spring.jpa.properties.hibernate.dialectorg.hibernate.dialect.MySQL8Dialect2.3 创建独立的前端 Vue3 项目打开终端在你喜欢的位置创建前端项目# 使用 Vite 官方模板创建 Vue3 TypeScript 项目 npm create vuelatest car-rental-frontend创建过程中根据提示选择需要的特性我建议勾上TypeScriptVue Router (用于页面路由)Pinia (用于状态管理)ESLint (代码检查)创建完成后进入项目并安装 Element Plus一个流行的 UI 组件库和 Axios用于 HTTP 请求cd car-rental-frontend npm install npm install element-plus axios npm install --save-dev types/node # 解决引入path模块时的类型问题前端项目结构如下car-rental-frontend/ ├── src/ │ ├── api/ // 存放所有接口请求函数 │ ├── assets/ // 静态资源 │ ├── components/ // 公共组件 │ ├── router/ // 路由配置 │ ├── stores/ // Pinia 状态管理 │ ├── views/ // 页面组件 │ ├── App.vue // 根组件 │ └── main.ts // 入口文件 ├── index.html ├── package.json ├── tsconfig.json └── vite.config.ts // Vite 配置文件2.4 关键一步配置开发环境下的跨域和代理这是前后端分离开发第一个要解决的问题。前端运行在http://localhost:5173后端运行在http://localhost:8080浏览器会因为同源策略阻止请求。在后端解决推荐在 Spring Boot 的配置类或控制器上添加CrossOrigin注解或全局配置一个WebMvcConfigurerBean。这是最直接的方式。在前端解决更灵活修改vite.config.ts配置开发服务器代理。这样前端请求/api开头的路径时会被代理到后端服务器完美解决跨域也模拟了生产环境 API 路径。// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], resolve: { alias: { : resolve(__dirname, src) } }, server: { port: 5173, // 前端开发服务器端口 proxy: { // 代理所有以 /api 开头的请求到后端服务器 /api: { target: http://localhost:8080, // 后端地址 changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) // 可选重写路径去掉 /api 前缀 } } } })配置好后前端代码中请求axios.get(/api/cars)在开发环境下就会被转发到http://localhost:8080/cars。现在你可以分别启动两个项目后端在car-rental-backend目录运行mvn spring-boot:run。前端在car-rental-frontend目录运行npm run dev。打开浏览器访问http://localhost:5173如果能看到 Vue 的欢迎页并且浏览器控制台没有跨域错误说明环境基本打通了。3. 核心业务设计与后端 API 实现一个可用的租赁系统至少需要车辆、客户、订单这几个核心实体。我们先设计数据库再实现后端的 CRUD API。3.1 数据库表设计简化版创建数据库car_rental_db然后考虑以下几张表车辆表 (car)存储车辆信息。CREATE TABLE car ( id BIGINT PRIMARY KEY AUTO_INCREMENT, license_plate VARCHAR(20) NOT NULL UNIQUE COMMENT 车牌号, brand VARCHAR(50) NOT NULL COMMENT 品牌, model VARCHAR(50) NOT NULL COMMENT 型号, color VARCHAR(20) COMMENT 颜色, daily_rate DECIMAL(10, 2) NOT NULL COMMENT 日租金, status VARCHAR(20) DEFAULT AVAILABLE COMMENT 状态AVAILABLE, RENTED, MAINTENANCE );客户表 (customer)存储客户信息。CREATE TABLE customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 姓名, phone VARCHAR(20) NOT NULL UNIQUE COMMENT 电话, id_number VARCHAR(50) UNIQUE COMMENT 身份证号, driver_license VARCHAR(50) COMMENT 驾驶证号 );租赁订单表 (rental_order)核心业务表关联车辆和客户。CREATE TABLE rental_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_number VARCHAR(50) NOT NULL UNIQUE COMMENT 订单号, car_id BIGINT NOT NULL COMMENT 车辆ID, customer_id BIGINT NOT NULL COMMENT 客户ID, start_date DATETIME NOT NULL COMMENT 取车时间, end_date DATETIME NOT NULL COMMENT 还车时间, total_amount DECIMAL(10, 2) COMMENT 总费用, status VARCHAR(20) DEFAULT ACTIVE COMMENT 状态ACTIVE, COMPLETED, CANCELLED, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (car_id) REFERENCES car(id), FOREIGN KEY (customer_id) REFERENCES customer(id) );3.2 创建 Spring Boot 实体类使用 JPA我们可以将表映射为 Java 实体类。以Car实体为例// entity/Car.java package com.example.carrental.entity; import jakarta.persistence.*; import lombok.Data; import java.math.BigDecimal; Entity Table(name car) Data // Lombok 注解自动生成 getter, setter, toString 等 public class Car { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false, unique true) private String licensePlate; Column(nullable false) private String brand; Column(nullable false) private String model; private String color; Column(nullable false, precision 10, scale 2) private BigDecimal dailyRate; Column(nullable false) private String status AVAILABLE; // 默认状态为可用 }同理创建Customer和RentalOrder实体。注意在RentalOrder中使用ManyToOne来关联Car和Customer。3.3 实现数据访问层和业务层Spring Data JPA 的强大之处在于你只需要定义接口不需要写实现。// repository/CarRepository.java package com.example.carrental.repository; import com.example.carrental.entity.Car; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; Repository public interface CarRepository extends JpaRepositoryCar, Long { // 可以根据车牌号查询 Car findByLicensePlate(String licensePlate); // 可以根据状态查询车辆列表 ListCar findByStatus(String status); }业务层 (Service) 负责处理具体的业务逻辑比如创建订单时计算费用、检查车辆状态等。// service/CarService.java package com.example.carrental.service; import com.example.carrental.entity.Car; import com.example.carrental.repository.CarRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; Service public class CarService { Autowired private CarRepository carRepository; public ListCar findAll() { return carRepository.findAll(); } public Car findById(Long id) { return carRepository.findById(id).orElse(null); } public Car save(Car car) { // 这里可以添加业务校验例如车牌号是否已存在 return carRepository.save(car); } public void deleteById(Long id) { carRepository.deleteById(id); } public ListCar findAvailableCars() { return carRepository.findByStatus(AVAILABLE); } }3.4 构建 RESTful API 控制器控制器 (Controller) 接收 HTTP 请求调用Service并返回 JSON 数据。// controller/CarController.java package com.example.carrental.controller; import com.example.carrental.entity.Car; import com.example.carrental.service.CarService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/cars) // 所有接口以 /api 开头方便前端代理 CrossOrigin(origins http://localhost:5173) // 允许前端跨域访问 public class CarController { Autowired private CarService carService; GetMapping public ListCar getAllCars() { return carService.findAll(); } GetMapping(/available) public ListCar getAvailableCars() { return carService.findAvailableCars(); } GetMapping(/{id}) public Car getCarById(PathVariable Long id) { return carService.findById(id); } PostMapping public Car createCar(RequestBody Car car) { return carService.save(car); } PutMapping(/{id}) public Car updateCar(PathVariable Long id, RequestBody Car car) { car.setId(id); // 确保ID一致 return carService.save(car); } DeleteMapping(/{id}) public void deleteCar(PathVariable Long id) { carService.deleteById(id); } }按照同样的模式完成CustomerController和RentalOrderController。对于RentalOrderControllercreateOrder方法会复杂一些需要校验车辆是否可用、计算租金等。启动后端应用使用 Postman 或浏览器访问http://localhost:8080/api/cars应该能看到返回的空数组[]因为数据库还没数据。这说明你的后端 API 已经可以工作了。4. 前端 Vue3 页面开发与状态管理后端 API 准备好后我们开始构建前端界面。我们将使用 Vue3 的组合式 API、Vue Router 和 Pinia。4.1 配置 Axios 和 API 层首先创建一个统一的 Axios 实例便于管理请求基地址和拦截器。// src/utils/request.ts import axios from axios const service axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API || /api, // 从环境变量读取开发时走代理 timeout: 5000 }) // 请求拦截器 service.interceptors.request.use( (config) { // 可以在这里统一添加 token // const token localStorage.getItem(token) // if (token) { // config.headers.Authorization Bearer ${token} // } return config }, (error) { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( (response) { return response.data }, (error) { // 统一处理错误例如提示消息 console.error(请求错误:, error) return Promise.reject(error) } ) export default service然后为每个实体创建对应的 API 模块。// src/api/car.ts import request from /utils/request import type { Car } from /types/car // 需要定义 TypeScript 接口 export function getCarList() { return request.getCar[](/cars) } export function getAvailableCars() { return request.getCar[](/cars/available) } export function createCar(data: Car) { return request.postCar(/cars, data) } export function updateCar(id: number, data: Car) { return request.putCar(/cars/${id}, data) } export function deleteCar(id: number) { return request.delete(/cars/${id}) }4.2 使用 Pinia 进行状态管理对于车辆列表这种需要在多个组件间共享的数据使用 Pinia 管理非常方便。// src/stores/car.ts import { defineStore } from pinia import { ref } from vue import * as carApi from /api/car import type { Car } from /types/car export const useCarStore defineStore(car, () { const carList refCar[]([]) const loading ref(false) const fetchCars async () { loading.value true try { const data await carApi.getCarList() carList.value data } catch (error) { console.error(获取车辆列表失败:, error) } finally { loading.value false } } const addCar async (car: Car) { try { const newCar await carApi.createCar(car) carList.value.push(newCar) return newCar } catch (error) { console.error(添加车辆失败:, error) throw error } } // 其他 actions... return { carList, loading, fetchCars, addCar } })4.3 构建车辆管理页面使用 Element Plus 组件快速搭建一个车辆列表和表单页面。!-- src/views/car/CarListView.vue -- template div classcar-management el-button typeprimary clickhandleAdd新增车辆/el-button el-table :datacarStore.carList v-loadingcarStore.loading stylewidth: 100% el-table-column proplicensePlate label车牌号 / el-table-column propbrand label品牌 / el-table-column propmodel label型号 / el-table-column propcolor label颜色 / el-table-column propdailyRate label日租金 / el-table-column propstatus label状态 template #defaultscope el-tag :typescope.row.status AVAILABLE ? success : danger {{ scope.row.status }} /el-tag /template /el-table-column el-table-column label操作 template #defaultscope el-button sizesmall clickhandleEdit(scope.row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(scope.row.id)删除/el-button /template /el-table-column /el-table !-- 新增/编辑对话框 -- el-dialog v-modeldialogVisible :titledialogTitle el-form :modelform :rulesrules refformRef el-form-item label车牌号 proplicensePlate el-input v-modelform.licensePlate / /el-form-item !-- 其他表单字段 -- /el-form template #footer el-button clickdialogVisible false取消/el-button el-button typeprimary clicksubmitForm确认/el-button /template /el-dialog /div /template script setup langts import { ref, reactive, onMounted } from vue import { ElMessage, ElMessageBox } from element-plus import { useCarStore } from /stores/car import type { Car } from /types/car const carStore useCarStore() const dialogVisible ref(false) const dialogTitle ref() const form reactiveCar({ licensePlate: , brand: , model: , color: , dailyRate: 0, status: AVAILABLE }) const formRef ref() // 页面加载时获取数据 onMounted(() { carStore.fetchCars() }) const handleAdd () { dialogTitle.value 新增车辆 Object.assign(form, { licensePlate: , brand: , model: , color: , dailyRate: 0, status: AVAILABLE }) dialogVisible.value true } const handleEdit (row: Car) { dialogTitle.value 编辑车辆 Object.assign(form, { ...row }) dialogVisible.value true } const handleDelete async (id: number) { try { await ElMessageBox.confirm(确认删除该车辆, 提示, { type: warning }) await carApi.deleteCar(id) ElMessage.success(删除成功) carStore.fetchCars() // 重新加载列表 } catch (error) { // 用户取消或删除失败 } } const submitForm async () { // 表单验证和提交逻辑 try { if (form.id) { await carApi.updateCar(form.id, form) ElMessage.success(更新成功) } else { await carStore.addCar(form) ElMessage.success(新增成功) } dialogVisible.value false carStore.fetchCars() } catch (error) { ElMessage.error(操作失败) } } /script按照这个模式你可以继续开发客户管理 (CustomerListView.vue)、订单管理 (OrderListView.vue)、订单创建 (CreateOrderView.vue) 等页面。使用 Vue Router 配置好页面路由。5. 项目打包与生产环境部署开发完成后我们需要将前后端整合部署到一个可以独立运行的应用中。5.1 前端项目打包在car-rental-frontend目录下运行npm run build这个命令会使用 Vite 将你的 Vue 项目打包生成一个dist目录里面是优化后的静态文件HTML, JS, CSS。5.2 将前端资源集成到 Spring Boot有两种主流方式方式一将 dist 目录拷贝到 Spring Boot 的 static 目录这是最简单的方式。将car-rental-frontend/dist目录下的所有文件复制到car-rental-backend/src/main/resources/static/目录下。然后修改 Spring Boot 的控制器将根路径和前端路由路径都指向index.html。// controller/HomeController.java package com.example.carrental.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; Controller public class HomeController { // 处理前端路由所有未匹配到API的请求都返回index.html GetMapping(value {/, /{path:[^\\.]*}}) public String forwardToIndex() { return forward:/index.html; } }方式二使用 Maven 插件在构建时自动集成更工程化的做法是使用frontend-maven-plugin在pom.xml中配置让 Maven 在打包时自动执行npm install和npm run build并将生成的dist内容复制到target/classes/static。这种方式保证了构建过程的可重复性。具体配置可以参考文章开头提到的 GitHub 示例项目。5.3 打包并运行完整应用在后端项目根目录执行mvn clean package这会在target目录下生成一个可执行的 JAR 文件例如car-rental-backend-0.0.1-SNAPSHOT.jar。运行它java -jar target/car-rental-backend-0.0.1-SNAPSHOT.jar现在访问http://localhost:8080你应该能看到部署好的汽车租赁系统前端页面并且所有 API 请求都能正常工作。至此一个完整的前后端分离的汽车租赁系统就搭建完成了。6. 开发调试与常见问题排查在实际开发中你肯定会遇到各种问题。这里列出几个高频问题及排查思路。6.1 前端请求后端 API 404 或跨域错误检查代理配置确认vite.config.ts中的proxy配置正确且后端服务正在运行在8080端口。检查后端 Controller 路径确认你的后端 Controller 类上的RequestMapping路径与前端请求的路径匹配。前端请求/api/cars后端应该是RequestMapping(/api/cars)。检查后端跨域配置确保CrossOrigin注解已添加或者全局 CORS 配置已生效。直接测试后端 API先用 Postman 或浏览器直接访问http://localhost:8080/api/cars确保后端 API 本身是通的。6.2 前端热更新HMR失效这在整合 Spring Boot 和 Vite 时有时会遇到。根本原因是开发模式下前端资源由 Vite 开发服务器提供但页面路由可能被 Spring Boot 拦截。确保开发模式分离开发时前后端完全独立运行。前端npm run dev(端口 5173)后端mvn spring-boot:run(端口 8080)。前端通过代理访问后端 API。不要直接访问 8080 端口开发时始终访问http://localhost:5173。直接访问 8080 端口看到的是打包后的静态文件没有热更新。检查 Vite 配置确保没有因为配置base或处理路由转发而破坏了 Vite 的 HMR 机制。我们的配置示例是可行的。6.3 数据库连接失败或 JPA 不创建表检查数据库配置确认application.properties中的数据库 URL、用户名、密码正确且数据库服务已启动。检查ddl-autospring.jpa.hibernate.ddl-autoupdate会在应用启动时根据实体类更新表结构。第一次启动后检查数据库中是否生成了对应的表。查看启动日志Spring Boot 启动时会在控制台打印大量日志仔细查看是否有数据库连接错误或 SQL 语法错误。6.4 打包后访问页面空白或 JS/CSS 加载 404检查静态资源路径打包后前端资源的路径是相对于index.html的。如果部署到非根路径例如/app/需要在 Vite 打包时配置base选项并在 Spring Boot 中做相应调整。确认文件已复制确保dist目录下的所有文件都已正确复制到src/main/resources/static/或target/classes/static/。查看浏览器开发者工具打开 Network 标签页查看加载失败的资源具体是哪个以及其请求路径从而定位问题。6.5 类型错误或编译错误前后端数据类型对齐确保前端定义的 TypeScript 接口如Car与后端实体类或 DTO 的字段类型匹配。例如后端的BigDecimal在前端对应number。检查依赖版本特别是 Spring Boot、Vue、Vite、TypeScript 的版本兼容性。使用相对稳定的版本组合。这个项目麻雀虽小五脏俱全。从环境搭建、技术选型、数据库设计、API 开发、前端组件、状态管理到最终打包部署覆盖了一个现代 Web 应用开发的主要环节。我建议你先按这个流程跑通理解每个部分的作用和连接方式。之后你可以在此基础上添加更多功能比如用户登录认证、权限控制、数据统计图表、文件上传驾驶证照片、短信通知、更复杂的计费规则等。每添加一个功能你都会对 Spring Boot 和 Vue3 有更深的理解。