车钥匙递过去的那一刻,业务才算真正开始——这是我做完这套Springboot汽车租赁系统后最深的感受。很多人以为租车系统就是“车辆表加订单表”的CRUD,真正动手才发现,从实名认证到车辆状态流转,从押金冻结到超时计费,每个环节都藏着业务细节。这篇文章我会从设计思路、数据库建模、核心模块实现到本地部署,把整个系统的关键决策和落地过程完整拆开讲,给正在做同类项目或准备接手汽车租赁系统开发的同学一份能直接参考的实操笔记。
这个系统面向两类人:一是做毕业设计或课程项目的学生,需要一套结构清晰、能跑通完整业务链路的代码;二是刚入行的后端开发者,想看看一个真实业务系统里订单状态机、并发防重、费用计算这些模块到底怎么组织。内容会覆盖Spring Boot的核心实践、MySQL表结构设计、Redis在业务中的真实用途,以及Windows和Linux两种环境下的部署细节,读完你应该能独立把类似系统搭起来。
1. 整体设计与技术选型思路
1.1 业务复杂度决定了技术栈
汽车租赁系统表面看是信息管理,实际上是一套带着强状态约束的交易系统。用户要注册、实名、选车、下单、支付押金、取车、还车、结算,管理员要管车辆档案、门店、订单、优惠、统计报表。这个链路决定了系统至少需要三个核心能力:可靠的用户认证、严格的订单状态管理、灵活的计费规则。
技术选型我用了Spring Boot 3 + MyBatis-Plus + MySQL + Redis + Spring Security的组合。Spring Boot 3相比2.x最大的变化是基线升级到Jakarta命名空间和Spring Framework 6,新项目直接用Boot 3可以少走一次未来迁移的弯路。MyBatis-Plus选它的原因是单表CRUD不需要手写SQL,像车辆管理、门店管理这类基础模块能省大量重复代码,复杂查询再通过自定义SQL兜底。Redis在这里不是摆设,车辆库存预占、用户登录Token、热点车辆缓存都靠它。
注意:Spring Boot 3要求JDK 17起步,如果你本机是JDK 8,要么升级JDK,要么老老实实用Spring Boot 2.7.x。这是很多新手上来就栽的第一个坑。
1.2 单体架构为什么够用
我没有拆微服务。汽车租赁系统这种业务规模,一个Spring Boot应用加一个MySQL实例完全能扛住,拆成微服务只会引入服务注册、配置中心、分布式事务这些和核心业务无关的复杂度。单体架构的好处是调试方便,IDE里一个应用直接跑起来,断点想打哪打哪,对学习和二次开发都友好。
项目结构上按模块分包而不是按技术层分包:controller、service、mapper这些是技术层,而user、car、order、store、coupon是业务域。我推荐按业务域组织代码,因为需求变更通常围绕某个业务域展开,比如给订单模块加个取消逻辑,你只需要动order包下的文件,而不是在十几个controller和service之间来回跳。具体分包方式是controller、service、mapper、entity、dto、vo、config、common并列,其中dto放请求入参、vo放接口返回结构、common放统一响应体、异常处理、工具类。
1.3 认证方案:JWT还是Session
租车系统有用户端和管理端,用户端需要支持手机号登录,管理端需要账号密码登录,两边都会调用后端接口。我用JWT做无状态认证,用户登录成功后后端签发一个token返回给前端,前端存在本地存储里,每次请求放到Authorization头里。
为什么不用Session?因为租车系统未来很可能要扩展小程序或App端,多端共用一套认证体系时,JWT的天然无状态特性让你不需要额外做Session同步。另外Spring Security的过滤器链处理JWT非常顺手,写一个OncePerRequestFilter,从请求头解析token,校验通过后把用户信息塞进SecurityContext,后续接口直接用注解拿当前用户即可。
Spring Security在Spring Boot 3里的配置方式和旧版差异很大,主要的坑在于lambda配置写法。比如放行注册登录接口:
java复制http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/api/car/list", "/api/car/detail/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
这段配置里的关键点是requestMatchers的顺序,/api/admin/**的权限规则必须放在anyRequest之前,否则会被后面的兜底规则覆盖导致失效。我一开始把顺序写反了,结果管理端接口所有人都能访问,排查了半天才发现是这个原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:租赁业务的核心是状态与快照
2.1 核心表结构拆解
整个系统的表设计,我梳理下来核心是这几张:用户表、车辆表、门店表、订单表、费用明细表。辅助表还有优惠券表、违章记录表、车辆图片表、操作日志表。
用户表字段除了常规的账号密码手机号,需要特别注意两个:一个是实名认证状态,因为租车必须实名,用户信息里要有姓名、身份证号、驾驶证号这些字段,认证状态用枚举区分未认证、审核中、已认证;另一个是信用分或黑名单标记,用于风控拦截。
车辆表是另一个重点。车辆不是简单的“有/没有”两态,而是有明确的状态机:可用、已预约、租赁中、维修中、已下架。车辆状态下单时必须做校验,防止多个人同时租同一辆车。车辆表还会冗余门店ID,方便按门店维度查询车源。
订单表是整个系统字段最多的表。除了订单号、用户ID、车辆ID、门店ID、下单时间这些常规字段,还要有预计取车时间、预计还车时间、实际取车时间、实际还车时间、订单金额、押金金额、订单状态。这里要解释一个设计点,为什么订单表要冗余车辆名称、车牌号、租金单价这些历史快照——因为车辆信息可能会变,比如调价了或者改涂装了,但历史订单必须保留下单那一刻的信息,关联查询车辆表会拿到当前值,历史数据就失真了。
费用明细表我单独拆了出来,记录租金、押金、超时费、违约金、优惠减免这些费用项,每笔订单对应多条费用记录。拆出来的好处是费用计算的逻辑可以独立维护,后续新增收费项不用动订单主表结构。
2.2 订单状态机设计
订单状态是租赁系统最核心的业务状态,我用一个整数字段存储,枚举定义如下:
| 状态值 | 含义 | 可流转到的状态 |
|---|---|---|
| 0 | 待取车(已下单待支付押金) | 1、5 |
| 1 | 使用中(已取车) | 2、3 |
| 2 | 待结算(已还车) | 3 |
| 3 | 已完成 | - |
| 4 | 已取消 | - |
| 5 | 已超时 | 2、3 |
这里需要重点说“已超时”这个状态。租车订单到了预计还车时间还没还车,系统不会自动把订单置为已完成,而是先标记超时,开始计算超时费用。我实现方式是一个定时任务,每分钟扫描一次使用中的订单,如果当前时间超过预计还车时间,就把订单状态从1改成5。还车时,如果订单处于超时状态,系统要多算超时费。
状态流转我会在Service层做一次合法性校验,写一个工具方法判断下一个状态是否合法。这么做的价值在于,后续不管是用户端接口还是管理端接口,只要走到订单状态更新,都必须过这个校验器,避免出现“已取消的订单又被标记为已完成”这种逻辑漏洞。
2.3 为什么需要Redis参与库存控制
租车系统的并发压力虽然没有电商秒杀大,但存在同一个时间窗口内多个人抢同一辆车的冲突。处理方案是Redis预占加数据库复核。
用户在选车页看到一辆车,“暂定”这个动作会把车辆ID和选车时间段写入Redis,以车辆ID加日期为key,写入一个短时有效期标记,同时设置分布式锁防止并发写入覆盖。下单成功后,把Redis里的预占标记删除,车辆状态在数据库里改成已预约。如果用户超时未支付,预占标记到期自动释放,其它用户就能重新选这辆车。
Redis用在这里还有一层作用:热门门店的车辆库存数据从缓存读取,减少数据库压力。车辆列表接口默认先查Redis,没命中再查MySQL并回填缓存,缓存key的设计是store:car:list:{storeId},门店车辆变更时主动删除对应缓存。
3. 核心模块实现与业务细节
3.1 用户实名认证流程
实名认证这块业务不难,但流程上有讲究。用户提交姓名、身份证号、驾驶证号,后端先把身份证号做格式校验,再看是否和姓名匹配。真实项目里一般会接第三方实名认证接口,我这里用一个模拟服务代替,校验通过就把认证状态改为已认证,不通过则记录原因。
这里有个关键的细节:实名信息和账号信息是分开存储的。账号表里只存登录凭据,实名信息放用户扩展表。为什么要分开?因为一个账号可能绑定多个驾驶证,或者后续要支持企业用户,把实名信息独立出去,扩展空间更大。查询用户详情时再做一次关联查询返回聚合后的VO对象。
3.2 车辆检索与可租校验
车辆列表接口是用户端调用频次最高的接口,支持按城市、门店、取车时间、还车时间、车型、品牌筛选。实现思路上,前几个条件是SQL动态拼接,可租校验是独立逻辑。
写一个车辆可租校验的工具方法:通过车辆ID加时间段去订单表查是否存在状态为待取车或使用中、且时间窗口重叠的订单。所谓时间窗口重叠,就是新订单的预计取车时间和预计还车时间与已有订单的时间段有交集。SQL写法核心是:
sql复制SELECT COUNT(*) FROM rental_order
WHERE car_id = #{carId}
AND order_status IN (0, 1, 5)
AND #{pickupTime} < expected_return_time
AND #{returnTime} > expected_pickup_time
这个判断逻辑是租车系统最容易写错的地方。一开始我用了反过来的条件,结果明明有冲突的单子也查不出来,导致同一辆车被租给了两个人。后来画了线段图才理解:两个时间段重叠,等价于新开始 < 旧结束 且 新结束 > 旧开始,这个逻辑记死就行。
3.3 订单计费:租金、押金、超时费的计算
计费逻辑我单独抽了一个FeeCalculator组件,输入是订单信息,输出是费用明细列表。租金计算规则是按天计价,不满一天的按一天算。比如日租金300元,用户租两天零三个小时,按三天算,租金900元。
超时费的计算规则是:超过预计还车时间后,前两个小时按日租金的20%每小时收取,之后按日租金的50%每小时收取,超时费封顶不超过当日租金。押金的计算和车辆价值挂钩,取车时冻结,还车时如果无违章无事故则自动解冻。
费用计算过程中的精度问题要特别注意,金额一律用BigDecimal,用double计算金额会在小数位产生精度丢失,这在涉及钱的系统里是不能容忍的。如果数据库字段已经用了DECIMAL,Java侧对应字段就声明为BigDecimal,换算时用setScale(2, RoundingMode.HALF_UP)保留两位小数。
3.4 取车还车操作的状态流转
取车操作是管理员在后台操作的。用户在门店出示订单和身份证,管理员确认后点击取车,后端把订单状态从待取车改成使用中,同时车辆状态改成租赁中。这里我会加一个登录人的操作日志记录,方便事后审计。
还车操作更复杂一些。正常还车流程是管理员检查车辆外观和里程,记录油量或电量,确认无新增损伤后点击还车,后端计算费用并生成结算单,订单状态改成待结算。如果用户之前超时了,还车时要额外计算超时费用。这些费用明细在还车结算后推送给用户端,用户确认无异议后订单流转到已完成。
3.5 权限管理与后台功能
管理端接口全部要求管理员角色,我用Spring Security的@PreAuthorize("hasRole('ADMIN')")做方法级权限控制。用户端和管理端的用户表我是一张表,通过角色字段区分。给用户和管理员各注册一个账号,登录的时候根据角色返回不同的菜单权限和首页信息。
后台功能模块我做了这几个:仪表盘统计(今日订单数、营收、车辆利用率)、车辆管理(增删改查上下架)、门店管理、订单管理(查询、取车、还车、取消)、用户管理(实名审核、黑名单)、优惠券管理、车辆违章记录管理。每个管理模块都是标准的列表加表单操作,MyBatis-Plus的分页插件在这里派上了用场。
4. 本地开发环境搭建与项目运行
4.1 JDK、Maven、MySQL、Redis的安装配置
拿到源码后第一步是准备环境。JDK 17是硬性要求,这里有个反而很多新手会漏掉的小配置:安装完JDK后需要在系统环境变量里配JAVA_HOME,不然命令行里java -version能跑但IDEA可能识别不到。Maven我用3.8以上版本,安装完要检查本机的Maven仓库配置,默认的中央仓库下载依赖比较慢,建议换成国内镜像源。
MySQL用8.0版本,字符集建库时选utf8mb4,排序规则选utf8mb4_general_ci。注意MySQL 8的驱动类名和连接串和MySQL 5.x不一样:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/car_rental?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: yourpassword
连接串里的serverTimezone=Asia/Shanghai必须加,不加的话连接时可能报时区错误。allowPublicKeyRetrieval=true是MySQL 8配合caching_sha2_password认证方式时需要的,不加会报Public Key Retrieval is not allowed。
Redis在Windows上的安装方式有点特殊,官方不直接提供Windows版本,通常用tporadowski的Redis for Windows或通过WSL安装。装好后默认端口6379,Spring Boot连Redis的配置很简单:
yaml复制spring:
data:
redis:
host: localhost
port: 6379
database: 0
timeout: 3000ms
4.2 数据库脚本导入与初始化数据
项目的sql目录下通常会有两个文件:schema.sql是建表语句,data.sql是初始化数据。导入顺序一定先建库再导入,直接在Navicat或MySQL命令行里执行均可。
导入后建议先查看一下核心表的数据量。系统自带的初始数据一般包含十几个城市、几十家门店、上百辆车、一个管理员账号。管理员账号密码在data.sql里通常有注释标注,或者直接看数据库里users表的内容。
提示:拿到源码第一步不要急着启动,先看项目的
application.yml,确认数据库账号密码和本地环境是否一致,Redis连接是否配置正确。很多项目跑不起来的原因不是代码问题,而是配置文件和本地环境不匹配。
4.3 IDEA导入项目与启动步骤
IDEA打开项目后,选择Open,定位到项目根目录的pom.xml,用IDEA的Open as Project方式导入,它会自动识别Maven项目并开始下载依赖。第一次导入依赖下载会比较慢,遇到依赖下载失败的情况,优先检查Maven镜像配置和网络。
Maven依赖下载完成后,先别急着启动,做两件事:第一,确认右侧Maven面板里项目能正常刷新无报错;第二,看看项目有没有lombok依赖,如果代码里大量使用@Data注解而IDEA没装Lombok插件,编译会直接报找不到getter/setter方法。
启动Spring Boot应用的方式有两种,直接运行主类里的main方法,或者用Maven命令:
bash复制mvn clean package -DskipTests
java -jar target/car-rental-0.0.1-SNAPSHOT.jar
启动看到“Started Application in x.xxx seconds”的日志说明后端起来了,默认端口一般配的是8080。前端项目如果是Vue写的,需要单独启动,开发模式下执行npm install然后npm run dev,浏览器访问前端地址。因为我做的是前后端分离的接口对接,前端的接口代理配置要检查一下,生产环境通常用Nginx把/api路径反向代理到后端8080端口。
5. 部署上线与常见问题排查
5.1 生产环境部署要点
本地跑通之后,部署到服务器上有几个值得注意的点。首先是配置外置,不要改jar包内部的application.yml,而是在启动命令里指定外部配置:
bash复制java -jar car-rental.jar --spring.config.additional-location=/opt/car-rental/application-prod.yml
这样可以做到代码和配置分离,换环境不用重新打包。
服务器上的MySQL和Redis建议用Docker方式部署,省去手工安装的各种坑。MySQL容器创建时挂载数据卷,避免容器删除后数据丢失:
bash复制docker run -d --name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-e TZ=Asia/Shanghai \
-v /opt/mysql-data:/var/lib/mysql \
mysql:8.0
Redis容器更简单,一条命令搞定。部署Java应用时,建议先用nohup方式做简单验证,再配置systemd服务实现开机自启。不管哪种方式,启动时加上JVM参数控制内存使用:
bash复制nohup java -Xms256m -Xmx512m -jar car-rental.jar \
--spring.config.additional-location=/opt/car-rental/application-prod.yml \
--server.port=8080 > /opt/car-rental/app.log 2>&1 &
5.2 高频报错与排查记录
整理一下我在这个项目里踩过的典型问题,给读者一份可以直接对号入座的排查清单。
端口被占用。启动报Port 8080 was already in use,常见于之前启动过应用没关干净,Windows下用netstat -ano | findstr 8080找到PID后结束进程。
数据库连接失败。报Communications link failure,优先确认MySQL服务是否启动、账号密码是否正确、连接串的参数是否完整。这个报错有个隐蔽原因:MySQL 8默认用了caching_sha2_password认证,但某些版本驱动连接时没传allowPublicKeyRetrieval=true,连上就断开。
Redis连接超时。报Unable to connect to Redis,排查Redis服务是否启动、端口是否可达、配置文件里有没有写错host。如果是服务器部署,还要检查云安全组有没有放行6379端口。
Lombok编译报错。报java: package lombok does not exist或者找不到getter/setter,在IDEA里安装Lombok插件,并在Settings里开启Annotation Processing。
跨域问题。前端调接口报CORS错误,要么在后端统一配置CorsFilter,要么通过Nginx反向代理解决。
5.3 系统二次开发与扩展建议
如果拿到源码后要加功能,我建议先花时间读懂三个模块:订单状态机、费用计算器、车辆库存预占逻辑。这三个是业务核心,只要把它们吃透了,加功能基本就是在此基础上叠操作。
后续想扩展的方向很多,比如接入真实的支付接口、增加电子合同签署模块、做车辆GPS定位、加消息推送。从代码结构角度看,这些扩展点我都预留了位置,支付的回调接口、GPS数据的接收接口等都有空的入口类,拿到源码的读者可以直接在这些位置上继续填逻辑。
6. 我做这个项目的几点心得
整个项目做下来,我最想分享的一个经验是:业务系统的复杂度不在技术选型,而在状态和约束。租车系统的状态机、并发防重、计费规则,这些才是代码里真正需要小心维护的部分。
第二个体会是,拿到任何一个带源码带数据库的项目,先理清核心表的关联关系,再去看代码。我会在纸上画一遍用户、车辆、订单、门店四张表的关系图,标注清楚每张表的关键状态字段和流转方向,有了这张图,读代码的速度会快很多。
最后一点是关于交付质量的,这类源码项目最怕的是“开发者本机能跑,别人拿到手跑不起来”。我在写这套系统时特别注意了这里,把所有依赖的外部环境控制在JDK、MySQL、Redis三样,初始化SQL脚本里尽量把基础数据准备齐全,前端的Node版本也用相对通用的版本。工具链越简单,别人接手时越不容易出问题。
