☰
SpringBoot+Vue租车管理系统开发实战:从设计到部署踩坑记录
2026/10/5 13:35:39 网站建设 项目流程

开头

做毕业设计或者公司内网小项目时,“租车服务管理系统”是特别容易撞车的题目,但也是最适合当时练手的场景。一套完整的租车系统要覆盖用户登录注册、车辆信息浏览、在线下单、订单审批、费用计算、车辆状态流转这些环节,正好能把SpringBoot的后端功底和Vue的前端交互能力都串起来。本文要聊的是我在实际落地这个项目时的完整思路和踩坑记录,从SpringBoot项目结构、MySQL表设计,到Vue路由、动态菜单、打包部署,尽可能把每一步的取舍和操作细节说清楚。适合准备做SpringBoot+Vue相关毕设的同学,也适合想自己搭一套完整前后端分离项目的入门开发者参考。

我建议你带着“如果我要把这个系统改成重新跑一遍”的心态来读,而不是光看功能清单。租车系统的核心难点不在于概念新,而在于业务状态机、金额计算和前后端权限联动这类细节写起来极其啰嗦。下面按我从设计到落地的实际顺序展开。

1. 需求梳理与功能模块划分

1.1 租车业务到底在管什么

很多人拿到这类题目第一反应是搞几张表、写几个增删改查页面就交差,但租车业务比表面看起来要复杂一些。核心角色至少有三个:普通用户(租车人)、管理员(门店/平台运营)、系统后台(负责审核与统计)。业务流程大体是这样:用户注册登录后浏览在租车辆,选择取还车时间后提交订单;管理员在后台审核订单、确认车辆可用并完成取车交接;用户归还后系统根据租用天数和单价计算费用,同时更新车辆状态。

这里面最容易被忽视的是“车辆状态”这个概念。它不是简单的“在库/租出”二选一,而是包含了可租、已预订、已取车、维修中、已下架等多个状态,而且状态之间不能随便跳。比如一辆车被下一条订单预约了,哪怕还没有正式取车,也应该从用户端隐藏或不可选,否则就会出现超卖。我在设计表结构时把状态字段独立出来,并且所有状态变更都走统一的服务方法,就是为了避免到处直接改字段。

1.2 功能模块清单

整理之后,功能模块大致如下:

  • 用户端:注册、登录、个人信息维护、车辆浏览与条件筛选、下单、我的订单列表、取消订单、还车申请。
  • 管理员端:用户管理、车辆信息管理(上下架、维修标记)、订单审核、订单列表查询与统计、租金结算、基础数据管理(品牌、门店)。
  • 公共模块:登录鉴权、统一异常处理、文件上传(车辆图片)、操作日志。

这些模块如果合并到一张架构图里看,本质就是“用户—订单—车辆”三个核心实体加上权限控制。后面的数据库设计和接口设计都围绕这三条主线走,不要贪多。很多同学喜欢一开始就加“优惠券”“保险”“违章押金”,结果数据关系越搞越乱,最后答辩时自己都讲不清。我的建议是先把主线做扎实,扩展功能放到最后的扩展点里提一嘴即可,后期有时间再补。

2. 技术选型与数据库设计

2.1 SpringBoot后端技术栈选择

后端我使用的是SpringBoot 2.7.x版本。之所以不用最新的SpringBoot 3.x,一方面是新版本要求JDK 17基线,很多学校的服务器和电脑还停在JDK 8;另一方面是SpringBoot 2.7仍然在主流维护期内,相关的集成资料、网上踩坑记录都比较全,更适合毕业设计或中小型项目。如果你用的是IDEA 2026这类新开发环境,创建SpringBoot项目时直接在Spring Initializr里选Java 8和SpringBoot 2.7.18即可,没必要迷信最新版本。

持久层我选了MyBatis-Plus而非原生MyBatis或JPA。原因很简单:租车系统的单表查询很多,MyBatis-Plus提供的内置方法能省掉大量重复的XML和Mapper代码。比如分页查询车辆列表,只要引入PaginationInnerInterceptor,写一个LambdaQueryWrapper就能搞定,不用手写Count查询和Limit拼装。如果后面需要复杂统计,比如“某个月门店收入”,再单独写XML里的SQL就行,完全够用。

权限认证方面我选了JWT(JSON Web Token)方案。前后端分离项目的Session处理比较别扭,JWT无状态、跨域友好,配合SpringBoot拦截器就能完成登录校验。要注意的是JWT的密钥要放在配置文件里,而不是写死在代码中,过期时间我设置的是两个小时,用户每次操作时前端可以在响应401后主动跳转登录页。

2.2 前端Vue技术栈选择

前端我用的是Vue 2 + Element UI,这个组合虽然不如Vue 3 + Element Plus新,但胜在稳定且资料多。如果是从零开始学,我建议你直接学Vue 3 + Element Plus,毕竟官方生态已经全面转向Vue 3。但我这里要强调一点,很多同学在Vue安装及环境配置这一步就卡住了,常见问题包括npm版本过老、node-sass安装失败、vue-router版本不匹配等。Vue 3项目建议直接使用Vite构建,node版本在16以上就行,用npm create vite@latest快速初始化,不要再用vue-cli那套老古董。

路由管理我使用Vue Router。考虑到租车系统的菜单权限是动态的,管理员和普通用户看到的侧边栏不一样,我这里实现了动态路由。具体思路是:前端只定义静态路由(登录、首页、404),用户登录后根据返回的角色标识和菜单权限列表,通过router.addRoute()动态注册业务路由。这个比把所有路由都写在静态文件里再靠v-if控制要好维护得多,页面刷新后也能从store或localStorage中恢复权限数据。

组件通信方面,涉及少量跨组件状态时我没上Vuex,直接用了一个简单的store模式(一个js文件导出reactive对象)。只有当用户信息、车辆列表筛选条件、订单状态等被多个页面同时使用时,才考虑Pinia或Vuex。过度设计在中小型项目里反而是负担。

2.3 数据库表结构设计

数据库我采用的MySQL 8.0,字符集统一utf8mb4。核心表我拆成了五张:用户表、车辆表、品牌表、订单表、操作日志表。不要把所有字段塞进一张表,也不要因为追求范式把车辆和品牌拆成完全无关的两张表,适度冗余更实用。

用户表大概长这样,核心字段为id、username、password(BCrypt加密)、phone、role(0普通用户、1管理员)、status(0禁用、1正常)。车辆表字段比较多,有id、brand_id、model_name、plate_number、daily_rate、status(0可租、1已预订、2已租出、3维修中)、cover_image、location、description。订单表是核心中的核心,字段包括id、order_no、user_id、car_id、start_date、end_date、total_days、total_amount、status(0待审核、1已确认/待取车、2使用中、3已完成、4已取消)、create_time等。

表关系上,订单表的user_id关联用户表id,car_id关联车辆表id,品牌表的id被车辆表brand_id引用。这些外键逻辑在业务上必须保证,但我实际建表时没有加物理外键约束,而是在服务层校验。原因有两点:一是物理外键在高并发插入或更新时有一定性能影响,二是MyBatis-Plus在逻辑删除和分页时对带外键的表有时会有额外限制。如果你担心数据一致性,可以通过事务和业务代码来保证。

我额外加了几个索引:订单表的user_id和status联合索引、车辆表的status索引、订单表的order_no唯一索引。这些索引是实际测试时发现的性能瓶颈,比如后台“待审核订单”列表页面,如果订单积累多了,没有索引时查询会全表扫描,后面数据一多页面响应就会明显变慢。

3. 后端SpringBoot核心实现

3.1 项目结构与统一响应

后端代码我按照“Controller—Service—Mapper—Entity”四层结构组织,Controller只做参数接收和响应包装,Service里放业务逻辑,Mapper负责数据库交互。下面是我常用的包结构:

  • controller:存放接口入口,如UserController、CarController、OrderController
  • service:业务逻辑接口和实现类,如OrderService、OrderServiceImpl
  • mapper:MyBatis-Plus的Mapper接口
  • entity:数据库实体类
  • common:统一响应类、异常处理、工具类
  • config:配置类,如WebMvcConfig、CorsConfig、InterceptorConfig

统一响应类是前后端联调的基础。我定义了一个Result 类,包含code、message、data三个字段。code为200表示成功,400表示业务错误,401表示未登录或token过期,500表示服务异常。前端axios拦截器里直接判断response.data.code,如果是401就清除本地用户信息并跳转登录页。

3.2 登录认证与拦截器

登录接口的逻辑不复杂,先按用户名查用户,再用BCryptPasswordEncoder校验密码,最后生成JWT返回给前端。JWT的生成代码大致如下:

String token = Jwts.builder() .setSubject(user.getId().toString()) .claim("username", user.getUsername()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 7200000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();

拦截器的核心作用是从请求头Authorization中取出token并校验,如果校验失败就抛出RuntimeException,然后由全局异常处理器统一捕获。放行的URL列表包括login、register、车辆查询列表等无需登录的接口,其余接口全部要过Token校验。这里有个小坑,如果前端请求时没有把token加到header里,后端拦截器会拦截所有请求导致404或401,所以前端封装axios时一定要在请求拦截器里统一加上Authorization字段。

3.3 车辆与订单核心业务逻辑

车辆管理的核心接口是分页查询、新增车辆、上下架和状态变更。分页查询我使用了MyBatis-Plus的分页插件,关键配置如下:

PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(50L);

新增车辆时,车牌号要做唯一性校验,图片上传用MultipartFile接收,然后存储到本地指定目录,数据库里保存访问路径。这里要说一下,开发阶段图片通常存到项目static/upload目录下,但部署到服务器后,如果用了打包成Jar的方式再访问static目录就会麻烦不少,因为jar内部的资源路径不是常规文件路径。更建议把图片存到服务器某个固定目录(比如/home/app/upload),然后配置一个虚拟路径映射,让前端可以通过/upload/**访问。

订单业务是最容易出错的地方。用户提交订单时,第一步要检查车辆当前是否可租,这里不仅要看status是否为0,还要检查该车辆在用户选择的日期区间内有没有时间冲突的订单。我加了一个“日期区间重叠查询”方法:查询所有未取消的订单,条件为取车时间小于等于用户还车时间,且还车时间大于等于用户取车时间,如果查出来有记录就拒绝下单。这个方法看起来很笨,但准确性很高,避免了只有状态判断导致的车被重复预订问题。

车辆状态变化我用了一个简单的状态机方法,所有状态迁移统一在OrderService里完成:

  • 用户下单成功后:车辆状态从0变为1(已预订)
  • 管理员审核通过后:车辆状态从1变为2(已取车/使用中)
  • 用户还车后:车辆状态从2变为0(可租),同时更新最后还车时间
  • 如果订单取消或审核不通过:车辆状态恢复为0

这样做的好处是状态迁移路径清晰,不会出现“车辆还是已预订状态但订单已经取消了”的脏数据问题。状态修改后要同步更新车辆表,这一步必须在同一个事务中完成,否则一旦中途报错就会出现订单和车辆状态不一致。

3.4 费用计算与订单状态流转

租金计算是租车系统里最需要仔细处理的点。我的计算规则是:总金额等于车辆每日租金乘以租用天数,租用天数按自然日计算,取车也算一天。比如用户取车是7月1日,还车是7月3日,实际计费天数就是3天。这里要注意Date类型的比较,如果直接用timestamp相减再除以一天的毫秒数,会因为时区问题出现一天的误差。我的做法是对LocalDateTime类型的日期先转成LocalDate,然后通过ChronoUnit.DAYS.between计算天数差,规避时区影响。

订单状态流转我设计了五档状态:0待审核、1已确认/待取车、2使用中、3已完成、4已取消。前端页面根据状态显示不同操作按钮,比如“待审核”状态下用户可以取消订单,“已确认”状态下用户不能直接取消,必须联系管理员。管理员在“使用中”状态确认还车后,订单直接跳到“已完成”,同时费用在这里最终确定。如果中间有超时还车,我在还车接口里做了一次额外费用计算,超出部分的单价按1.5倍计算,这个逻辑也可以在后期扩展成催缴功能。

4. 前端Vue核心实现

4.1 项目初始化与环境配置

Vue项目初始化后一定要先处理两件事:安装依赖和配置代理。npm install比较慢时建议设置国内镜像源,但我不建议全局覆盖registry,而是用.npmrc文件单独配置。代理配置放在了vue.config.js里,开发环境中前端请求统一走 /api 前缀,然后代理到后端地址,这样可以避开CORS跨域问题:

devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } }

这个代理配置还能帮你调试时免去在后端写CorsConfig的麻烦。不过需要注意的是,部署到生产环境后,代理就不生效了,要么用Nginx做反向代理,要么前端直接把baseURL改为后端能够跨域访问的完整地址。两种方案我调研后最终选择了Nginx统一代理静态资源和后端接口,省心很多。

4.2 路由设计与权限控制

路由设计上我采用静态路由加动态路由的结合。静态路由只包含login、register、home、404,其他业务页全部走动态注册。用户登录后,后端返回一个roles字段和一个菜单树结构,前端根据菜单树生成对应的路由对象,再调用router.addRoute()添加。

动态路由的实现难度不高,但有个坑:页面刷新后动态路由就没了,因为Vue实例会重新加载,localStorage里的菜单树如果没有同步恢复,会导致刷新后页面空白。我的解决方法是:在main.js入口或App.vue创建前,先读取localStorage中保存的menuData,如果能读到就恢复路由,再挂载应用。这个过程虽然有点“土”,但实测下来很稳。

Vue Router还涉及导航守卫。我写了全局前置守卫,逻辑是:如果目标路由是业务页面且未登录,跳转登录页;如果已登录且访问登录页,则跳转首页。这里要注意在next()前把每个页面的标题处理好,不然浏览器标签页永远不会变。

4.3 核心页面与组件拆分

用户端首页我拆成了顶部导航栏、车辆列表、筛选栏、分页四个组件。车辆列表用el-card展示,每张卡片包含车辆图片、车型、日租金、状态标签,点击“立即预订”后跳转到车辆详情页。详情页除了展示信息,还需要一个日期选择器,用于取还车时间选择,并实时计算租金。这里有一个交互小细节:日期选择器要禁用过去的日期,且选择还车日期时自动判断是否早于取车日期,这些逻辑全部做成计算属性,避免在模板里堆复杂判断。

管理员端我使用了经典的侧边栏布局,左侧菜单从动态权限列表生成,右侧内容区域放路由出口。组件复用方面,我写了通用的分页组件和状态标签组件。比如状态标签组件,根据传入的状态值自动决定显示什么背景色和文字,用户端订单状态与管理员端共用,这个小组件帮我省了不少重复代码。

4.4 状态管理与接口封装

状态管理虽然没上Vuex,但我把用户信息、token、车辆筛选条件放在了一个全局store里。简单场景直接使用Vue提供的reactive对象加export就可以,类似下面这样:

import { reactive } from 'vue' export const store = reactive({ userInfo: null, token: localStorage.getItem('token') || '' })

接口封装是前端高效开发的关键。我把所有axios请求按业务模块拆分文件,比如user.js、car.js、order.js,每个文件里的方法都返回Promise。后面页面只需要import对应方法,不需要关心URL和header。axios拦截器里统一处理loading状态和错误弹窗,这是我强烈建议的,否则每个请求都要写一遍错误提示,代码会非常冗余。

5. 联调部署与常见坑

5.1 前后端联调的雷区

联调阶段我遇到的第一个问题是日期格式不一致。后端返回LocalDateTime默认是类似“2025-06-15T10:30:00”的格式,而前端Element UI的日期选择器需要的是“2025-06-15”。如果直接绑定会显示NAN。我在后端统一加了Jackson配置,将LocalDateTime序列化格式改为“yyyy-MM-dd HH:mm:ss”,同时前端也定义了一个dateFormat过滤器,双保险处理。

第二个问题是跨域。开发阶段通过devServer代理能绕过去,但生产环境一旦前后端分开部署,必须在后端配置CorsConfig。配置时记住allowedOrigin不要用“*”,因为带Cookie的请求不允许使用通配符,建议把前端域名写进去。如果使用了JWT无状态认证,跨域配置相对宽松一点,但还是要小心预检请求(OPTIONS),拦截器要对OPTIONS请求直接放行。

第三个大坑是时间字段在前端格式化后又出现偏移。这个问题困扰了我一整天,原因是Jackson默认序列化时会把LocalDateTime按UTC时区输出,而前端取到后按本地时区解析,导致时间差了8小时。解决方案是配置统一的时间格式化以及时区为GMT+8,前后端都按北京时间来处理。

5.2 打包部署与SpringBoot集成

部署方式我尝试过两种,一种是前后端完全分离部署,前端打包后的dist目录由Nginx托管,后端打jar包跑在Java进程上。另一种是把前端打包后放进SpringBoot的static目录里,让SpringBoot统一提供页面和接口。第二种方案特别适合学生毕设部署或者内网项目,因为只需要一个Java进程,不用单独安装Nginx。

实现方式很简单:先执行npm run build,然后把dist目录下的所有文件复制到SpringBoot项目的src/main/resources/static目录下,重新打包jar即可。要注意的是dist目录下的index.html引用的JS、CSS路径需要用相对路径,否则在SpringBoot里访问会因路径前缀导致404。同时,Vue Router要使用hash模式而不是history模式,因为history模式在SpringBoot后端没有配置View controller时,刷新页面会直接404。实际配置中我加了一个简单的WebMvcConfig,把前端无法匹配的路由转发到index.html,配置如下:

registry.addViewController("/{path:[^\\.]*}").setViewName("forward:/index.html");

这个配置可以避免刷新后白屏的问题。不过要注意,如果接口路径也匹配到这个规则,会被错误转发到index.html,所以SpringBoot的Controller优先级更高,实际测试中接口会被正常处理,因为这个ViewController匹配的是视图控制器层面的路径,和@RestController的接口映射不冲突。

5.3 常见问题速查表

这里把我实际开发中遇到的高频问题和解决方案整理成一张速查表,方便大家抄作业。

问题现象原因解决办法
前端请求接口返回404代理没生效或路径错误检查vue.config.js中的pathRewrite和target,请求路径是否带 /api 前缀
提交订单后车辆状态不变事务没生效或状态更新逻辑写错检查ServiceImpl上是否加了@Transactional,打印SQL日志定位更新语句
用户订单列表为空查询条件拼错,通常是多表join时字段名不对用MyBatis-Plus的LambdaQueryWrapper拼条件,注意是实体字段名而非列名
刷新页面后路由404Vue Router用了history模式改为hash模式,或在后端配置forward到index.html
图片上传后访问不到虚拟路径映射未配置在WebMvcConfig中添加addResourceHandlers映射本地目录
密码明文存储安全问题改用BCryptPasswordEncoder,登录时校验加密结果
日期参数绑定失败JSON格式与后端LocalDateTime格式不匹配后端配置Jackson的LocalDateTime序列化格式,前端统一提交字符串
跨域请求被拦截后端未配置CORS添加CorsConfig,allowedOrigin写前端实际地址

5.4 一点额外的经验

如果你准备把这类系统当毕设交,建议在答辩时重点讲两个点:一是订单日期冲突检测的逻辑,二是动态路由权限控制。这两块是很多类似项目里“设计得比较浅”的地方,你只要把代码讲清楚,老师一般都会认可。另外,日志打印一定不要省,尤其是订单操作、状态变更这类关键流程,至少要打印出参数和结果,这样后期排错会容易很多。

我在实际开发中还发现,车辆列表的缓存问题很隐蔽。用户端下完订单后回到列表页,车辆状态可能还是旧数据。这个问题的根源是车辆列表接口做了简单的Redis缓存,但订单状态变化后没有主动清理缓存。我的做法是当订单状态变化时,直接调用缓存删除方法清空对应车辆详情和列表缓存。这一块代码量不大,但能避免很多因为缓存宕出导致的状态不一致。

6. 后续可以做的扩展

整个系统跑通之后,有几个方向很值得扩展。租车公司往往需要统计报表功能,可以在后端基于订单表做聚合查询,比如按门店、按月统计订单量和收入,前端用ECharts画柱状图和折线图。还有消息通知功能,订单审核通过后给用户发短信或邮件,这里可以使用SpringBoot整合WebSocket实现站内信,或者对接第三方短信服务。

另一个必要的扩展是限流和操作日志审计。虽然毕设项目不需要很高的并发,但接口限流能体现你对生产环境的理解。我之前用拦截器加了一个简单的接口访问计数,按IP每分钟限制请求次数,超过就返回提示。管理员的删改操作写入操作日志表,查询时按时间倒序展示,这样哪怕系统被误操作了也有追溯依据。

最后,文件存储方面如果你的项目要处理大量车辆图片,本地目录存储不是长久之计。可以把图片转到对象存储服务或者MinIO这类私有化部署方案里,前端通过临时URL访问。这个改动不涉及核心业务表结构,主要是文件上传和访问路径的适配,在系统稳定后可以随时替换。

我个人在实际操作中的体会是,租车系统这类“业务感很强”的小项目,真正拉开差距的往往不是某个炫酷的功能,而是订单状态是否闭环、金额计算是否严谨、刷新页面是否白屏这些细节。把这些细节一个一个磨平,你不仅能把项目按时交付,还会积累很多在书本上很难学到的工程经验。

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

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

立即咨询