门店租车最怕的不是车不够而是明明有车却不知道车在哪订单要靠Excel手工登记计费靠口算还车时超时费扯皮。这套基于SpringBootVue的租车服务管理系统就是把“车辆、订单、用户、计费”整套流程线上化从租车预约、取车、还车到费用结算全部在系统里闭环跑完。不管是做毕设还是给中小型租车门店做信息化改造这套东西都很值得参考。我会按实际开发顺序来拆先把业务想清楚再聊架构设计、数据库建模然后重点讲后端订单状态机和计费引擎怎么写前端页面怎么和接口配合最后把我开发过程中踩过的坑和排查过程梳理一遍。1. 项目定位与核心需求拆解1.1 租车业务到底在管什么租车服务管理系统本质上是一个“资产流转”系统核心管理对象是车核心业务动作是“把车交出去”和“把车收回来”。在中间这一段车处于什么状态、被谁使用、该收多少钱这三个问题必须实时回答清楚。我接手这个项目时门店上午登记租车下午还车全靠前台手写一张租车单车型、车牌、里程、油量全靠人眼核对。遇到两辆车同时看中同一台车谁先谁后也凭前台印象。这种模式的问题不是效率低而是数据一旦丢就全乱了计费和库存根本无法追溯。所以在设计系统时我没有先急着写代码而是把业务拆成三条主线车辆生命周期管理、订单全流程跟踪、费用自动计算。用户注册登录后可以浏览车辆、提交租车订单管理员负责审核订单、办理取车还车、维护车辆信息财务和店长需要看到每日营收和车辆利用率。1.2 核心功能模块与角色权限划分系统的角色我划分成了三种普通用户、门店管理员、系统管理员。普通用户就是客户主要用C端功能门店管理员操作车辆和订单系统管理员管用户和权限。我整理了一张功能清单模块功能点使用角色用户模块注册、登录、个人信息、押金账户用户、管理员车辆模块车辆新增/编辑、上下架、状态跟踪管理员订单模块用户下单、管理员审核、取车还车登记用户、管理员计费模块按小时/按天计费、超时费、优惠计算系统自动统计模块订单量、营收、车辆利用率报表管理员、老板系统管理角色权限、操作日志、公告设置系统管理员订单模块是核心中的核心它串起了用户、车辆、金额三个实体。用户提交订单时车辆必须是“可租”状态管理员确认取车后车辆状态变成“租用中”还车时系统根据实际时长和里程计算总费用。1.3 非功能性需求的现实约束除了业务功能还要考虑并发和异常场景。节假日是租车高峰期多个用户同时抢同一辆车的情况非常常见。如果系统不做并发控制库存或车辆状态就可能被超卖。另外租车系统的计费涉及金额不能有半点马虎。数据库事务一定要用好特别是还车结算时需要同时完成订单状态更新、车辆状态恢复、费用记录写入任何一个环节失败都不能留下半截数据。这些约束在需求阶段不明确到了后期就是大坑我后面专门有一节讲这个。2. 架构设计与数据库建模2.1 为什么选择SpringBootVue的前后端分离方案选择SpringBootVue最重要的原因是生态成熟、招聘和参考资料多。SpringBoot让后端开发不需要再折腾复杂的XML配置内嵌Tomcat打好包就能跑Vue的前端组件化开发非常适合管理后台这类交互密集型的项目。前后端分离另一个好处是可以分别独立开发和部署。后端只提供RESTful接口前端通过HTTP请求拿数据。我这边前后端是两个端口在跑前端开发服务器走代理转发到后端生产环境再通过Nginx把接口代理过去整个链路非常清晰。有人会问为什么不直接用JSP或Thymeleaf模板渲染。我只能说如果你只需要做一两个简单页面模板渲染确实够用但租车管理涉及车辆列表筛选、订单表单、状态流转提示、统计图表页面交互复杂度高Vue的响应式数据绑定和组件复用会让开发体验好太多。2.2 技术栈选型清单后端我选的是SpringBoot 3.x搭配MyBatis-Plus做数据持久层。很多人纠结用JPA还是MyBatis我的习惯是业务查询相对复杂、需要自己控制SQL的项目用MyBatis-Plus它提供了单表CRUD封装复杂查询又能写XML两边都不耽误。前端是Vue 3 Vite Element Plus Pinia。Vite的冷启动比Webpack快太多Element Plus的表格、表单、弹窗组件对后台系统来说完全是量身定做。状态管理用Pinia简单直接没有Vuex那么多样板代码。数据库用的MySQL 8.0Redis作为缓存和分布式锁的候选方案但真正常用的还是MySQLRedis主要存验证码这类临时数据。鉴权方案是JWT Spring Security密码加密用BCrypt。2.3 核心表设计与关系说明数据库设计是项目的地基我一开始就把表结构敲定了后面改动就少了。核心表有六张用户表、车辆表、订单表、费用表、角色表、操作日志表。表名关键字段说明t_userid, username, password, phone, role_id用户基础信息t_carid, car_no, brand, model, daily_price, status车牌唯一状态区分可租/出租中/维修t_orderid, order_no, user_id, car_id, start_time, end_time, status订单核心表t_feeid, order_id, amount, type, create_time费用明细区分租金/押金/违约金t_roleid, role_name, permissions角色和权限t_logid, user_id, action, detail, create_time操作日志订单表和车辆表之间是外键关联但我在实际代码中没有单纯依赖外键约束而是通过应用层逻辑保证关系。比如订单存在时不允许删除车辆这样控制更灵活也避免数据库约束引起不必要的麻烦。车辆表的st字段我用了tinyint0代表可租1代表出租中2代表维修3代表下架方便SQL查询和索引优化。关键查询表都要加索引。订单表我建了(user_id, status)联合索引车辆表给status单列索引统计报表查询再按月创建时间走索引。数据量不大的时候索引效果不明显但等系统跑了一两年订单数据积累到几万条之后没有索引的查询会直接拖垮页面响应。3. 后端从骨架到业务闭环3.1 统一响应体、异常处理与日志后端开发的第一步不是写业务代码而是搭好公共模块。我定义了一个统一的返回结构Result包含code、message、data三个字段。所有Controller接口都返回这个结构前端拿到之后统一判断code是否为200再决定是渲染数据还是弹错误提示。配合统一返回体我还加了全局异常处理器。业务异常、参数校验异常、未知异常分别处理。业务异常通过自定义的BusinessException抛出前端可以看到明确的错误信息比方说“该车辆已被预约请选择其他车辆”未知异常则返回“系统繁忙请稍后再试”避免把堆栈信息暴露给用户。日志这块我用了SLF4J关键操作都打点跟踪。创建订单、审核通过、还车结算都记录操作人和操作内容。前期也许觉得日志无所谓真出了线上问题日志就是唯一的线索。租车项目涉及钱审计追踪是必须的。3.2 登录鉴权与角色控制登录这块我直接用Spring Security做过滤链JWT做无状态认证。用户登录成功后后端生成一个包含用户ID和角色信息的Token返回给前端前端每次请求在Header里带上这个Token后端通过过滤器解析Token并放入SecurityContext。密码加密必须用BCrypt不要自己写MD5加盐。MD5虽然也能加密但暴力破解成本低BCrypt内部自动加盐同样的密码每次生成的哈希都不同安全性高很多。角色控制上接口上用PreAuthorize注解做方法级鉴权。比如车主增删改接口只允许ADMIN角色访问普通用户角色调用就直接返回403。Spring Security的注解方式非常方便不需要在每个Controller里手动判断角色。3.3 车辆管理与库存扣减车辆管理本身不复杂难点在于“防止同一辆车被重复下单”。用户查询车辆列表时看到的是可租状态但在他浏览的这几分钟里其他人可能已经提交了订单。我最初的做法是用户提交订单时后端先检查车辆状态是否为可租然后更新车辆状态为“锁定”。但这里存在并发问题两个请求同时查到状态为可租同时执行更新就可能都成功。解决办法是使用乐观锁或Redis分布式锁。我最后选了数据库层面的悲观锁对车辆记录执行SELECT ... FOR UPDATE锁住这条记录后再判断状态并更新。这个方案在单库环境下最直接可靠Redis分布式锁虽然也行但引入额外组件还要考虑锁超时和重入问题对于项目规模来说反而复杂。核心代码思路如下// 在事务方法中 CarEntity car carMapper.selectByIdForUpdate(carId); if (car.getStatus() ! CarStatus.AVAILABLE) { throw new BusinessException(该车辆已被预约); } car.setStatus(CarStatus.LOCKED); carMapper.updateById(car);这里要注意FOR UPDATE必须放在事务里才生效而且查询条件必须走主键或索引避免锁住整个表。我在排查一个问题时发现如果条件字段没有索引MySQL会锁全表那性能就废了。3.4 订单状态机与计费引擎订单状态我用一个数字字段表示0代表待取车1代表租用中2代表已完成3代表已取消。取车时管理员操作让订单从0变成1同时车辆状态变成出租中还车时订单从1变成2车辆恢复为可租。这里最容易出的问题是状态跳转校验不严。我见过有系统直接UPDATE订单SET状态2对于已取消的订单也能还车成功这就是状态机没有做约束导致的。我在Service层写了状态流转校验方法只有当前状态等于允许的前置状态时才能推进否则抛异常。计费引擎是租车系统里最有意思的部分。我设计了一套规则取车时间和还车时间系统自动计算小时数不足半天按小时计费超过半天按天计费。租金单价根据车辆类型和租期档位各有不同。超时费单独计算超过预定还车时间2小时内不额外收费超过2小时按一小时的租金单价收。计算过程全部放在Service层金额用BigDecimal绝对不用double或float否则一分钱误差都解释不清楚。订单金额、实收金额、优惠金额分开存要能追溯到每笔费用是怎么算出来的。4. 前端页面、状态与接口联动4.1 Vue工程初始化与路由设计前端这块我用Vue 3的Composition API来写。项目的目录结构按模块划分views下面有user、order、car、stats等子目录公共组件放在components里API请求统一放在api目录下。路由分为两部分一部分是用户端页面包括首页车辆展示、车辆详情、下单页面另一部分是管理后台车辆管理、订单管理、统计报表。登录后的用户访问后台页面时通过路由守卫检查角色权限没有权限直接跳转首页。路由守卫是前端权限控制的一个重要环节。我在全局前置守卫里面判断目标路由的meta信息如果meta要求登录但本地没有Token就跳去登录页如果meta要求管理员角色但用户角色不是就提示无权访问。后端接口也做了同样的权限校验前端守卫只是提升体验不让用户看到闪一下的无权限页面。4.2 登录态管理与Axios封装前端和所有后端接口通信都要经过Axios我封装了一个统一的request对象。它做的事情很多从Pinia里取出Token并设置到请求头响应拦截器判断HTTP状态码和业务状态码401时自动清理登录信息并跳转登录页其他错误统一弹出消息提示。这里有一个细节Token过期往往会遇到拦截器里面跳转登录页和路由守卫同时触发处理不好就会死循环。我的处理方式是拦截器里只执行清理动作通过标记变量防止重复跳转跳转逻辑统一交给路由守卫处理。Axios封装好之后前端写接口调用就非常清爽了。每个模块的API文件里只保留函数定义页面组件只管调用并处理返回值。这样后端接口一旦有变化只需要改动一个文件。4.3 核心页面和组件实现用户端比较核心的是车辆列表页和订单提交页。车辆列表用卡片形式展示品牌、型号、日租金、车辆状态标签支持按车型和价格区间筛选。单个车辆的状态变化通过接口刷新不需要手动刷新整个页面。订单提交页是另一个重点。用户选择取车时间和还车时间后系统自动计算预估费用并显示在页面上。这里前端展示的预估价和后端结算的实际价是有可能不同的如果超时还车就会产生额外费用。我在界面上标注清楚“预估费用”结算结果以后台实际计算为准避免后期扯皮。管理后台的订单管理用Element Plus的表格组件支持分页、状态筛选。每一行订单旁边根据状态显示不同操作按钮比如待取车显示“确认取车”租用中显示“确认还车”已完成显示“查看明细”。这种按钮操作本质是调用后端状态机接口前端只控制界面层不放业务逻辑。4.4 统计报表可视化统计报表给管理员的日常运营提供决策依据。我用ECharts做了三个图表每日订单量折线图、近七天营收柱状图、车型热度饼图。这些数据通过后端统计接口返回接口在MySQL里按时间分组聚合。图表组件封装成了独立组件父页面只需要传配置项和数据。ECharts的响应式容器要给它设置固定高度否则初始化宽高会变成0图表显示不出来这个问题我当时排查了很久才发现是容器高度问题。5. 开发中踩过的坑与排查过程5.1 并发抢单导致车辆状态被覆盖压力测试时发现两台车同时被下单最后都显示成功。原因是两个请求同时进入订单创建方法虽然代码里判断了车辆状态但判断和更新之间不是原子的请求A没提交事务之前请求B读到的是旧状态。解决过程我前面提到了使用SELECT FOR UPDATE把记录行锁住。但还有一点容易漏就是加锁方法所在的整个链路必须在同一个事务里。我当时把锁查询和订单插入拆到两个方法因为Spring的Transactional默认只对同一个类内的方法调用生效结果锁一样没用。后来把所有业务操作合并到一个事务方法里问题才消失。排查这个问题时要同时看方法调用链和事务管理器配置缺一个都会出问题。5.2 还车时间计算差一小时计费模块测试时发现选下午14:00取车当天13:00还车系统计算时长却是负数。问题出在日期解析逻辑上前端传过来的是“2025-06-10 14:00:00”后端把字符串转成LocalDateTime时格式用错了导致解析出来的时间比实际晚了一小时。排查时看日志里的时间数值一眼就能发现不对劲。日期时间处理在租车系统里非常敏感我已经形成习惯所有时间字段统一用LocalDateTime数据库字段用datetime类型前后端传输用“yyyy-MM-dd HH:mm:ss”格式字符串。尽量不要用Date类型它有很多时区相关的坑。5.3 前端响应拦截器跳转死循环Token过期后Axios拦截器跳转到登录页但登录页的登录和刷新接口本身也会走拦截器结果就是在登录页和接口之间反复横跳。当时浏览器卡到没法操作控制台刷满了401状态码。解决办法是设置一个标记当已经在跳转过程中时后续请求直接丢弃同时对白名单接口不做401跳转比如获取验证码的接口。这个问题告诉我们拦截器不是越强越好要区分接口的场景。5.4 MyBatis-Plus分页查询返回总数为0用MyBatis-Plus的分页插件查询订单列表数据能正常显示但总条数始终是0分页组件显示不出正确页数。排查发现是分页插件没有正确配置拦截器。MyBatis-Plus的分页依赖PaginationInnerInterceptor必须在配置类里显式注入。我一开始只引入了依赖忘了配置拦截器分页SQL只能查出当前页的数据拿不到count。还有一个小细节分页插件要写在多数据源或租户插件的后面否则拦截顺序不对也可能出问题。5.5 跨域和代理配置问题前端开发时访问后端接口直接请求后端地址会触发跨域。我在Vite的配置里设置了proxy代理所有/api开头的请求转发到localhost:8080并且在后端也配置了CORS允许跨域。需要注意如果同时开了Vite代理和后端CORS有时候会出现加过一层跨域头又加一层的现象虽然能跑但很不规范。我的建议是开发环境只用代理后端CORS只针对生产环境不走同一网关时的场景。配置越简单越不容易出错。6. 落地部署与后续扩展方向6.1 打包部署的关键步骤后端部署我用Maven打包成Jar包放到服务器上用systemd服务管理。配置文件中数据库连接、JWT密钥、日志路径都放在外部配置里不写死。前端打包后生成的静态文件放到Nginx的html目录Nginx里配置location /api指向后端地址。有一个必须要做的事就是JWT密钥不能暴露在代码仓库里。我在环境变量里注入JWT_SECRET启动命令里引用这样即使代码泄漏密钥也拿不到。6.2 按这个基础还可以继续扩展什么系统目前版本已经满足日常运营了但如果继续演进有几个很自然的扩展点。第一个是引入消息队列处理高峰期的订单通知用户提交订单成功后通过异步消息发送短信或邮件。第二个是车辆GPS定位对接在订单租用中状态实时显示车辆当前位置一方面给用户安心感另一方面便于企业风控。第三个是接入支付押金和租金直接通过支付网关实现线上扣款不用线下转账。还有一点现在的权限是基于角色的角色-权限模型可以根据实际需要继续往细了拆比如操作级权限和字段级权限每个管理员只能看到和操作与自己相关的部分。6.3 最后分享一点我的实操体会整套系统的开发工作量大头不在增删改查而在业务规则的边界处理。租车系统里订单状态永远不是单一跳变计费规则也不是固定公式它们随着业务模式调整而调整。所以写代码的时候把状态流转和计费逻辑单独抽出来做成引擎是非常值得做的前期设计。我自己做完这版之后最大的感受是表结构定义清楚后端业务和数据库事务对齐前端再把接口和页面交互捋顺整个项目就没有想象中那么难。希望这篇内容对准备做租车系统的朋友有帮助能少踩一些我踩过的坑。