我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

SpringBoot+Vue3全栈实战:汽车租赁系统开发深度解析

SpringBoot+Vue3全栈实战:汽车租赁系统开发深度解析 做汽车租赁系统这个项目之前我在选型上纠结了挺久。市面上主流的方案无外乎 SSM、SpringBoot 单体、以及 SpringBootVue 前后端分离这么几类。最终我敲定了Java SpringBoot Vue3 MyBatis MySQL这套组合不光是冲着它们在 GitHub 和面试题里出现的频率高更关键的是这套技术栈做出来的系统结构清楚、好维护、也方便以后扩展。这篇文章我就以自己实际开发这个汽车租赁系统的过程为主线把核心设计思路、表结构、接口实现、前端页面交互以及我踩过的那些坑都拿出来跟你捋一遍希望能给正在做毕业设计、或者想练手全栈项目的朋友一点参考。1. 项目概述与整体设计思路很多人一拿到“汽车租赁系统”这种题目第一反应就是“增删改查”但真动手做起来才发现光是业务边界和状态流转就能绕晕人。我建议在写代码之前先静下心来把系统到底服务于谁、要解决什么核心问题想清楚再画一下角色、功能模块和核心流程后面写代码会顺手很多。1.1 核心需求解析汽车租赁系统说到底解决的是三件事车怎么展示、订单怎么租、车怎么还。围绕这三件事系统里的角色也分得比较清晰。游客/未登录用户能浏览车辆列表、查看车辆详情但下单前需要注册登录。已登录用户客户可以修改个人资料、提交租车订单、支付押金/租金、还车、查看自己的订单记录、评价车辆。管理员负责车辆信息的上架下架、审核订单、处理还车结算、管理客户账号、查看运营数据比如热门车型、出租率。我在做需求分析的时候遇到一个比较典型的矛盾点到底要不要做“审核”环节如果做了用户下单后还要等管理员确认流程更真实但代码复杂度上来了如果不做直接下单自动通过又感觉少了点业务感。我的处理方式是折中——生成订单时将车辆状态的校验放在后端管理员可以在后台对异常订单进行关闭操作而正常订单走“已确认 → 使用中 → 已完成”的自动流转这样既保证了流程完整性又不会因为审核卡住用户的体验。核心业务流程我梳理成了这样用户注册/登录 ↓ 浏览车辆 → 选择车辆 → 提交订单填写取车/还车时间 ↓ 车辆状态校验 租金计算 ↓ 生成订单(状态:待取车) → 订单关联车辆状态改为已预定 ↓ 取车(管理员/自动) → 订单状态:使用中 → 车辆状态:租赁中 ↓ 还车 → 计算费用含超时费 → 订单状态:已完成 → 车辆状态:可预约这个流程里车辆状态和订单状态其实是分开的、但又互相联动我在后文会详细说怎么用状态字段控制这里先有个整体概念就行。1.2 为什么选择前后端分离方案这个项目我选的是前后端分离架构换句话说前端 Vue3 项目和后端 SpringBoot 项目是两个独立的工程部署的时候也是分开的它们之间通过JSON HTTP 接口通信。对比一下传统单体 JSP/Thymeleaf 方案前后端分离的优势还是挺明显的对比维度前后端分离Vue3 SpringBoot单体模板方案Thymeleaf/JSP开发分工前端工程、后端工程独立开发前端代码耦合在后端项目里接口调试前端可 Mock 数据后端可 Postman 调试页面渲染依赖后端数据部署方式前端静态资源可部署 Nginx/CDN后端独立部署打成一个 jar/war 包整体部署维护难易各层职责清晰改动影响可控改个页面样式可能都要重启后端学习价值更贴近企业实际项目节奏偏传统适合快速验证技术原理用一句话总结我自己的体会前后端分离不是给自己找麻烦而是让“前后端各自擅长的事”分开做。Vue3 负责页面交互和组件化开发SpringBoot 只聚焦暴露稳定的 RESTful 接口和数据校验数据库只干自己该干的存取活。对做毕设或项目演示来说这套结构也能让你在答辩时讲出更多层次。2. 技术选型解析这套组合为什么能扛住需求选型不是看哪个火就选哪个而是要回答几个问题团队/个人熟不熟、生态全不全、遇到问题时好不好查资料、以及部署体积可不可控。下面我把四个核心组件逐个聊一聊。2.1 后端核心SpringBoot 为什么比传统 SSM 更省心如果你用过 SSHStruts2 Spring Hibernate或者 SSMSpring SpringMVC MyBatis一定体会过那一堆 XML 配置的酸爽。SpringBoot 最大的贡献就是把“约定优于配置”发挥到了极致内嵌 Tomcat不用再去外面装容器一个 main 方法就能把整个 Web 应用拉起来。在这个项目里我选 SpringBoot 还有几个很实在的理由自动装配能力引入spring-boot-starter-web、mybatis-spring-boot-starter之后大部分配置都有默认值我只需要在application.yml里写数据源、MyBatis 的 mapper 扫描路径这些关键项。生态成熟排错成本低SpringBoot 的报错信息、官方文档和社区问答非常多新手遇到“端口占用”“数据源配置错误”这类问题时基本上搜一下就能找到答案。内置 Actuator 和健康检查虽然小项目用不到太花哨的监控但/actuator/health可以快速确认服务是否存活部署到服务器上排查问题时非常实用。2.2 持久层选型MyBatis 还是 JPA这几乎是 Java 后端项目里最常被问到的问题之一。我自己在这个项目里选了MyBatis原因是它的SQL 控制粒度更细尤其是租赁系统里那些多表关联查询、状态统计报表写 SQL 反而比 JPA 的自动拼接更直观可控。举个例子查询“可预约车辆列表”时我需要同时关联车型、门店和车辆状态select idselectAvailableCars resultTypecom.lease.entity.vo.CarVO SELECT c.id, c.car_no, c.brand, c.model, c.daily_price, c.status, t.type_name AS car_type_name, s.name AS store_name FROM car c LEFT JOIN car_type t ON c.car_type_id t.id LEFT JOIN store s ON c.store_id s.id WHERE c.status 0 AND c.deleted 0 ORDER BY c.daily_price ASC /select如果是用 JPA虽然也能用Query注解写 JPQL但一旦涉及复杂嵌套查询、动态条件拼接MyBatis 的where、if、foreach标签用起来要顺手得多。而且 MyBatis 可以直接复用 MySQL 优化过的 SQL 语句排查慢查询时直接拿日志里的 SQL 去 EXPLAIN路径非常短。2.3 前端核心Vue3 Element Plus 的搭配逻辑前端这部分我用了Vue3 Vite Pinia Vue Router Element Plus。Vue3 和 Vue2 最大的差异是 Composition API 和基于 Proxy 的响应式系统写起来更灵活逻辑复用也更优雅。对于汽车租赁系统这种需要大量表单、表格和状态管理的中后台项目Element Plus 则提供了一套现成的组件库让我不用从零造轮子。这里我要说一下为什么用 Vite 而不是 Vue CLI。如果你用的是旧项目可以忽略这一点但新开项目我强烈建议 ViteVite 基于原生 ES Module冷启动速度非常快改代码后的热更新几乎是秒级响应开发体验比 Webpack 时代的 Vue CLI 舒服太多了。命令也简单npm create vitelatest car-rental-front -- --template vue cd car-rental-front npm install npm run dev跑起来之后打开http://localhost:5173就能看到 Vite 默认的欢迎页接着再安装 Vue Router、Pinia 和 Element Plus 就好。2.4 数据库选型MySQL 5.7/8.0 怎么选MySQL 是这个系统的数据底座。我在实际项目中用的是MySQL 8.0因为 8.0 对窗口函数、通用表表达式CTE和默认字符集utf8mb4的支持更好而且性能优化方面比 5.7 又有提升。如果你是在自己电脑上做练习电脑配置不高装 MySQL 5.7 也一样跑得动核心 SQL 基本兼容。安装配置这里给个关键心得字符集一定要在初始化时指定 utf8mb4不然存中文、emoji 或特殊符号时容易出现乱码。MySQL 8.0 的安装包在官网就可以下载Windows 上用 ZIP 免安装版也可以配置好环境变量后在命令行依次执行mysqld --initialize-insecure mysqld install net start mysql mysql -u root -p如果是第一次用initialize-insecure会让 root 用户初始密码为空登录后马上设置新密码ALTER USER rootlocalhost IDENTIFIED BY 你的新密码; FLUSH PRIVILEGES;3. 实操过程从零搭建汽车租赁系统这一节我按“环境准备 → 数据库初始化 → 后端工程 → 前端工程”的顺序把搭建过程完整走一遍。每一步我都会强调容易出错的地方。3.1 环境准备与版本选择先把我的环境版本贴出来给大家一个参考组件版本备注JDK1.8 / 11SpringBoot 2.x 用 JDK8 没问题SpringBoot 3.x 需要 JDK17SpringBoot2.7.x稳定版适配 JDK8社区资料多Vue3.x配合 Vite 5 使用Node.js18我用的 18npm 自带MySQL8.05.7 也行MyBatismybatis-spring-boot-starter 2.3.x注意与 SpringBoot 版本兼容IDEA2023.2装了 Lombok、MyBatisX 插件这里要特别提醒一个最容易翻车的点SpringBoot 版本和 MyBatis starter 版本不匹配。如果 SpringBoot 升到了 3.x那么mybatis-spring-boot-starter也必须用 3.x 以上反过来SpringBoot 2.7 配 MyBatis starter 2.3.x 是比较稳的。搞混了就会出现启动时提示找不到SqlSessionFactory之类的诡异错误。3.2 数据库设计与初始化 SQL数据库名我起的是car_rental核心表一共 6 张CREATE DATABASE IF NOT EXISTS car_rental DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE car_rental;下面是各表的核心字段设计精简版实际建表时你还可以加 created_at、updated_at、deleted 这类通用字段用户表 sys_userCREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), role TINYINT DEFAULT 0 COMMENT 0-客户 1-管理员, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 用户表;车辆信息表 carCREATE TABLE car ( id INT PRIMARY KEY AUTO_INCREMENT, car_no VARCHAR(20) UNIQUE NOT NULL COMMENT 车牌号, brand VARCHAR(50) NOT NULL COMMENT 品牌, model VARCHAR(50) NOT NULL COMMENT 车型, daily_price DECIMAL(10,2) NOT NULL COMMENT 每日租金, car_type_id INT COMMENT 车型分类ID, store_id INT COMMENT 门店ID, status TINYINT DEFAULT 0 COMMENT 0-可预约 1-已预定 2-租赁中 3-下架, image VARCHAR(255), deleted TINYINT DEFAULT 0 ) COMMENT 车辆表;订单表 order_infoCREATE TABLE order_info ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, user_id INT NOT NULL, car_id INT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-待取车 1-使用中 2-已完成 3-已取消, actual_amount DECIMAL(10,2) COMMENT 实际结算金额, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 订单表;其余像门店表、车型分类表、订单评价表就按实际需要补充外键关联即可。建表之后再往里面插入几条测试数据比如别克GL8、丰田卡罗拉、大众速腾这几类常见车型方便后面调试接口和页面。3.3 后端工程搭建步骤在 IDEA 里新建 SpringBoot 项目勾选 Web、MySQL Driver、Lombok 这几个依赖然后手动加入 MyBatis。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.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyapplication.yml里我习惯把配置写得一目了然server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/car_rental?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.lease.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置我建议一定打开它能自动把数据库的user_id映射成 Java 里的userId省掉大量resultMap的手写工作。工程结构上我分了这几层com.lease ├── controller # 接口层 ├── service # 业务层 ├── mapper # MyBatis接口 ├── entity # 数据库实体对象 ├── vo # 视图对象给前端返回的 ├── common # 工具类、统一返回体、异常处理 └── config # 配置类CORS、拦截器等一个典型的分页查询接口Controller 层长这个样子RestController RequestMapping(/api/car) public class CarController { Resource private CarService carService; GetMapping(/list) public ResultPageResultCarVO list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String brand) { return Result.success(carService.queryCarPage(pageNum, pageSize, brand)); } }3.4 前端工程搭建与核心页面实现前端项目的目录结构我按业务模块划分src ├── api # 接口封装axios请求 ├── assets # 静态资源 ├── components # 通用组件 ├── router # 路由配置 ├── stores # Pinia状态管理 ├── views # 页面组件 │ ├── home # 首页/车辆展示 │ ├── order # 订单相关 │ ├── admin # 后台管理 │ └── user # 个人中心 └── utils # 工具函数请求封装我直接用一个 axios 实例// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { ElMessage.error(登录状态已过期请重新登录) localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default request这样写的好处是业务页面里再也不用重复写错误处理接口调用直接拿数据// 车辆列表接口示例 export function getCarList(params) { return request.get(/car/list, { params }) }首页车辆卡片展示用 Element Plus 的el-cardel-row/el-col就很方便。租车下单时用户选择起止日期前端把日期传给后端后端再计算租金。这里需要注意日期跨天的判断我下面会重点说。4. 核心业务模块与接口实现细节业务模块做得好不好主要看状态流转和边界情况处理得到不到位。这一节我把车辆管理、订单流程、权限控制这三个重点拆开讲。4.1 车辆管理模块状态字段是灵魂车辆状态status我用数字表示不搞复杂状态机0-可预约用户能看到并能下单1-已预定有订单生成但还未取车此时车辆在列表中要标记为不可下单前端可以置灰后端也要校验2-租赁中用户已取车车辆被占用3-已下架维修、报废或后台主动下线之所以把“已预定”单独列出来是为了解决多订单并发问题。举个例子用户 A 和用户 B 同时抢同一辆车两个人都提交了订单如果系统不做任何并发控制很可能出现超卖。我的处理方式是乐观锁——在订单表插入之前先执行更新语句把车辆状态从0改成1影响行数为 0 就说明被别人抢占了UPDATE car SET status 1 WHERE id #{carId} AND status 0利用“更新行数”来判断是否抢车成功这比先查询再判断要可靠得多。这里建议你用事务包裹整段逻辑避免出现订单创建成功但车辆状态没改过来的情况。4.2 订单流程模块租金计算与超时处理租金计算是租赁系统的核心数值逻辑。常规做法是按天计价public BigDecimal calculateAmount(LocalDate startDate, LocalDate endDate, BigDecimal dailyPrice) { long days startDate.until(endDate).getDays(); if (days 1) { throw new BusinessException(还车日期必须晚于取车日期); } return dailyPrice.multiply(BigDecimal.valueOf(days)).setScale(2, RoundingMode.HALF_UP); }这里有两个隐藏问题第一LocalDate.until()返回的是Period要调用getDays()之前要先注意如果跨月getDays()拿到的只是天数的差值不是总天数。正确做法是ChronoUnit.DAYS.between(startDate, endDate)比如 1月31日到2月2日ChronoUnit.DAYS.between得到 2但Period.getDays()拿到的可能就是 2因为相差1个月2天其实日部分对不上非常容易踩坑。第二如果用户还车时间超过了订单原定还车日期是直接拒绝还是允许超时归还并加收超时费我在系统里选择了“允许超时还车并加收超时费”的方式订单状态保留为“使用中”超时费按日租金的 1.5 倍计算并在订单详情里展示。这部分规则你可以按自己需求调但一定要在需求文档里写清楚不然前端和后端对“超时规则”理解不一致后面联调起来会非常痛苦。订单状态的流转图我用文字描述一下待取车(status0) → 使用中(status1) → 已完成(status2) ↓ ↓ 已取消(status3) 超时未还(status1 actual_amount累计)管理员在后台可以主动取消“待取车”订单这会触发车辆状态回滚为“可预约”。用户还车后后台执行结算把实际金额写入actual_amount订单状态更新为“已完成”。4.3 权限控制JWT 拦截器实现登录与角色区分用户和管理员的权限必须区分不然任何人都能调管理接口这个系统就失去意义了。我采用的方案是JWT 登录 后端拦截器校验角色。登录接口的逻辑大致是public String login(LoginDTO dto) { SysUser user userMapper.findByUsername(dto.getUsername()); if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { throw new BusinessException(用户名或密码错误); } // 生成JWTpayload里放 userId 和 role String token JwtUtil.generateToken(user.getId(), user.getRole()); return token; }后端配置一个拦截器对所有/api/**请求做 token 校验但对/api/auth/login和/api/car/list这些公开接口放行。再用一个简单的自定义注解RequireRole(role 1)标注需要管理员权限的接口拦截器里通过反射读取注解进一步校验角色。前端这边路由配置里加一个全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { ElMessage.warning(请先登录) next(/login) } else { next() } })这样前端做了一层体验上的拦截后端做了一层安全上的拦截两边同时把关才稳。5. 常见问题与排查技巧实录做项目最耗时间的不是写功能而是排查各种玄学问题。下面这份问题清单全部是我自己在这个项目里实际遇到过、并且花了时间才定位到原因的你可以直接当“避坑手册”用。5.1 高频报错速查表现象可能原因解决办法SpringBoot 启动报Failed to configure a DataSource没配置数据源或依赖缺了检查application.yml的 datasource 配置确认mysql-connector-j依赖已引入启动报Invalid bound statement (not found)Mapper 接口和 XML 对不上查看mapper-locations是否正确XML 文件是不是放在resources/mapper下用 MyBatisX 插件直接跳转定位接口返回 500日志里Unknown column xxx_id实体字段与数据库列名映射失败确认是否开启map-underscore-to-camel-case或者手动加TableField/Column前端请求就报 CORS 跨域后端没有配置跨域放行写一个WebMvcConfigureraddCorsMappings里放行对应路径和请求头同一个 List 接口偶尔返回旧数据本地起了多个后端实例端口被占用或缓存检查端口占用加Cacheable的接口注意缓存失效策略前端npm run dev很慢依赖不完整或 Node 版本过旧建议 Node 18删掉node_modules重新npm install数据库中文乱码字符集不一致建库用utf8mb4JDBC URL 加characterEncodingutf8订单创建时提示Too many connectionsMySQL 连接数打满连接池配置合理控制maximum-pool-size确认代码里没有线程池泄漏5.2 我踩过比较深的三个坑第一个坑是MyBatis 的if标签判断数字类型。有时候接口传参status是 0我写if teststatus ! null and status ! 结果死活不上条件。原因很简单数字类型的 0 和空字符串比较时会转成字符串判断这个写法对数字类型就不该写! 。后来我统一改成if teststatus ! null AND c.status #{status} /if只要判断非空即可不要再和空字符串比较尤其对 Integer/Long 类型的参数要特别注意。第二个坑是前端日期传参格式化问题。我用el-date-picker选完日期后默认拿到的是一个 Date 对象直接放到 query 里传给后端后端用LocalDate接收时发现格式不对。我的解决方法是统一用dayjs格式化后再传// 统一为 yyyy-MM-dd const formatDate (date) dayjs(date).format(YYYY-MM-DD)后端接口用DateTimeFormat(pattern yyyy-MM-dd)来接收这样两端格式就对上了。如果你是用 JSON 请求体传对象则要在实体字段上放JsonFormat(pattern yyyy-MM-dd)。第三个坑是登录密码明文存储。这不算 bug而是安全问题。我在第一次做这个项目的时候直接明文存密码后来被朋友指出来如果部署到公网这基本等于裸奔。后面我改用 Spring Security Crypto 的BCryptPasswordEncoder来做加密校验注册时加密存库登录时匹配加密串。改动成本不高但对系统的安全性提升是质变Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }5.3 联调与部署建议前端跑在5173后端跑在8080联调的时候有两种方式处理跨域和代理。开发阶段我建议在vite.config.js里配置代理避免每次都在后端写跨域配置// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })部署阶段前端把静态资源构建一下npm run build生成 dist 目录后交给 Nginx后端打成 jar 包后用java -jar car-rental.jar启动。再配合 Nginx 反向代理让前端页面和后端接口共用同一个域名这样可以规避掉很多跨域相关的线上问题。6. 项目可扩展方向与最后的小建议如果这个汽车租赁系统你想继续扩展我建议优先考虑三个方向一是增加支付模块对接微信/支付宝沙箱环境把押金和租金结算串起来业务更闭环二是增加消息通知利用 Spring Boot 的异步事件把下单、还车、结算等信息通过邮件或短信发给用户三是引入 Redis 缓存把车辆列表、首页轮播、热点车型这些高频读接口缓存起来顺带可以解决集群会话共享的问题。最后再分享一个我自己的体会做这类全栈项目先跑通最小闭环再加功能和细节这一点特别重要。不要一开始就想着把权限、日志、缓存、事务、分布式全部装满先让“用户能租车、管理员能管车”这一条主链路走通再逐步补强健壮性。这样开发过程中遇到问题定位范围会小很多心理压力也会小很多。对还在学习阶段的朋友来说一个能完整跑起来、逻辑清晰的项目比一个到处是半成品的功能堆砌更有学习和答辩价值。
返回列表