做毕业设计选题目说是技术活其实更大程度上是个体力活。我这两年陆陆续续帮学弟学妹看过不少题目发现“SpringBoot汽车租赁管理系统”几乎每隔一两个月就会有人问一次要是往共享汽车运营、在线租赁这个方向再靠一靠基本就是最热那批选题之一。为什么大家扎堆选这个我总结下来就三条业务闭环清晰车辆、用户、订单三个核心实体一摆出来功能边界就很明确技术栈主流SpringBoot加MySQL这一套出去面试也不掉价而且租金计费、订单追踪、状态流转这些点既好写代码也好写论文还能现场演示。这篇文章就把我从需求拆解到数据库设计、再到核心代码实现和答辩准备的完整过程铺开讲包括那些只有实际写了才会发现的坑目标是让你拿来就能搭出一套能跑、能讲、能答辩的轻量级汽车租赁系统。1. 项目规划先把业务想透再决定怎么写代码1.1 一眼看清系统要跑通的核心闭环我拿到这类题的第一件事从来不是新建工程而是把业务闭环在白纸上画一遍。汽车在线租赁无论包装成什么时髦名字核心链条都逃不开这么几步用户注册登录后浏览车辆确认车辆当前可租选择用车时间段下单接着到约定地点取车使用结束还车系统按时长算钱订单最终归档用户可以随时看到订单跑到哪一步。这套闭环里最容易做过头的就是功能堆砌。我见过不少同学一上来就规划了积分商城、优惠券、社区评论、消息推送听着很齐全结果数据库建了二十多张表核心链路反而没时间打磨。我的建议很直接毕业设计追求的是“在一个完整闭环里把质量做扎实”而不是“功能列表凑满一整页”。共享汽车运营平台确实可以加GPS定位、远程开关锁、分时计价这些点但前提是你先把租赁订单闭环做对了。额外功能最多挑一个能演示、能在答辩时说清楚原理的加上去加太多只会拉长开发周期、稀释论文深度。1.2 技术栈为什么不选最花哨的而选最稳的这套系统我首推的组合很朴素SpringBoot 2.7 MyBatis-Plus 3.5 MySQL 8.0 Redis JWT前端用Vue配合Element UI。前端如果实在没时间也可以用简单的服务端渲染页面关键是后端逻辑要经得起提问。每个选型背后都是有理由的不是顺手拿来的SpringBoot的核心价值是自动配置不需要维护一堆XML工程结构一眼清楚。而且starter机制本身就是一个很好的答辩知识点把SpringBootApplication的自动配置原理讲清楚比讲老框架的配置文件有亮点得多。MyBatis-Plus让单表增删改查和分页几乎不用写SQL省下来的时间全部砸到订单和状态流转这些真正的业务难点上。实体类的TableName、TableId注解也直观代码量肉眼可见地少。MySQL必须选InnoDB引擎因为它支持事务和行级锁。这里不是随便选的后面做并发租车控制行锁是核心手段面试和答辩都爱从这切入。Redis在毕设里不一定非用不可但我建议至少用在一个真实场景里比如存储登录态黑名单、缓存热点车辆列表。目的是证明你不只会调用redisTemplate还能讲清楚它解决的是什么问题。JWT做登录鉴权理由后面专门展开。我把这套组合和传统方案对比过差别主要体现在开发效率和可讲解性上环节传统SSM方案本方案为什么推荐本方案工程配置XML配置一大摞自动配置加application.yml省时间出错少数据库操作手写大量MapperXMLMyBatis-Plus通用CRUD把精力留给复杂SQL登录态Session加CookieJWT无状态Token跨端友好好讲原理并发控制简单查重InnoDB行锁加事务从根上防超租1.3 模块划分用户端和管理端拆开职责就清楚了模块设计只需要记住一个原则用户端只做“一个人租车”需要的事管理端做“运营一辆车”需要的事两边不要混。用户端核心功能注册登录密码必须加密存储这里用BCrypt绝对不要明文存。车辆浏览展示可租车辆支持按品牌、车型筛选和分页响应里带上价格和当前状态。在线下单选择起止时间系统做可用性校验生成待取车订单。我的订单订单列表、详情、取消操作、取车和还车操作按钮。模拟结算还车时按实际用车时长计算金额并展示明细。管理端核心功能车辆管理车牌、品牌、每小时租金、状态维护。订单管理全量订单查询按状态筛选处理用户反馈的异常订单。用户管理用户列表和状态操作。数据统计近期订单量趋势、热门车型排行用ECharts画两个图足够。这样划分完前后端接口数量也就清楚了开发时可以并行推进。我记得有次帮人改代码发现他把“修改车辆信息”和“用户下单车”写在同一个controller里接口路径混乱不说权限逻辑也拧成一团。所以模块边界一定要在动工前定死后面才不会返工。2. 数据库设计状态字段和表关系定了项目就成功一半2.1 三张核心表加两个设计原则不要一上来就设计十几张表。最少可用版本是三张表用户表user、车辆表car、租赁订单表rent_order。再加一张可选的操作日志表用于记录管理端的关键操作这一张表在论文里能作为“系统安全与可追溯性”的素材。用户表CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(32) NOT NULL COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT BCrypt加密后的密码, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, real_name VARCHAR(32) DEFAULT NULL COMMENT 真实姓名, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;车辆表CREATE TABLE car ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, plate_no VARCHAR(16) NOT NULL COMMENT 车牌号, brand VARCHAR(32) NOT NULL COMMENT 品牌, model VARCHAR(64) DEFAULT NULL COMMENT 车型, price_per_hour DECIMAL(10,2) NOT NULL COMMENT 每小时租金, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可租 1已租 2维修 3下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 上架时间, PRIMARY KEY (id), UNIQUE KEY uk_plate_no (plate_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆表;租赁订单表CREATE TABLE rent_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL COMMENT 用户ID, car_id BIGINT NOT NULL COMMENT 车辆ID, start_time DATETIME NOT NULL COMMENT 计划用车开始时间, end_time DATETIME NOT NULL COMMENT 计划用车结束时间, actual_start_time DATETIME DEFAULT NULL COMMENT 实际取车时间, actual_end_time DATETIME DEFAULT NULL COMMENT 实际还车时间, total_amount DECIMAL(10,2) DEFAULT NULL COMMENT 结算金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待取车 1用车中 2已完成 3已取消 4超时关单, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_car_id (car_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租赁订单表;两个设计原则必须记住金额一律用DECIMAL订单号给唯一索引。只要跟钱打交道的字段就别用float或double订单号是用户和客服核对订单的凭证唯一索引能从数据库层面防止重复单。另外每张表都带上status和create_time后面做筛选和统计全靠它们。2.2 车辆状态和订单状态为什么要分开维护这是新手最容易搞错的地方。有人为了省事在车上加一个“是否已租”的布尔值订单取消就置false还车也置false看起来很简洁但一旦出现维修、下架、超时关单这些情况布尔值根本表达不了。车辆状态描述的是“这辆车现在能不能被租”属于运营维度的概念订单状态描述的是“这个租赁合同走到哪一步”属于流程维度的概念。两者有联动关系但不是一回事。最简单的联动关系可以写成三段车辆0可租下单成功后变为1已租对应订单是待取车。 车辆1已租还车完成后变为0可租对应订单是已完成。 车辆1已租订单取消后变为0可租对应订单是已取消。把联动关系列出来以后你会发现所有状态变更都能收敛成几个固定动作锁车、放车、解锁、归档。开发时把这几个动作做成事务内的私有方法谁都不允许绕过事务直接改status字段状态就乱不了。答辩时问状态一致性你就把这个联动逻辑画出来基本不会被难住。这里要提一个设计上的边界上面这套单字段车辆状态隐含了“下单即占用车辆”的假设。如果要做未来预约比如我今天订车明天下午才取车辆就不能只用一个状态字段表达了因为同一辆车在“当前”和“预约时段”的可租性不同。干净的做法是车辆表只保留运营状态可租性改为动态计算查询目标时间段内是否存在未结束的冲突订单。这个点可以作为论文里的拓展方向答辩时主动提出来是明显的加分项。2.3 时间重叠判断一个SQL搞定的核心校验租赁系统最核心的校验就是时间段冲突。两个时间段一个是从s1到e1另一个从s2到e2它们有重叠的充要条件是什么很多人第一反应是写一堆边界判断其实核心就两个条件s1小于e2同时s2小于e1。自己在数轴上画一画就能验证这个条件把所有重叠场景都包含了。边界上用严格小于还能避免“恰好还车时间等于另一个人取车时间”被误判成冲突。落到SQL就是SELECT COUNT(*) FROM rent_order WHERE car_id #{carId} AND status IN (0, 1) AND #{startTime} end_time AND #{endTime} start_timestatus取0和1是因为待取车和用车中的订单都占用了车辆的未来或当前时段。只要这个查询结果大于0就直接拒绝下单。这个SQL写对90%的“超租”问题就不会发生。做演示时这也是一个效果很好的场景用两个账号同时下同一辆车的重叠时段系统必须把第二个请求拦下来。3. 核心功能实现从搭建工程到订单状态流转的完整落地3.1 工程骨架与关键依赖工程结构用最常规的分层com.example.rental ├── controller // 接口层 ├── service // 业务层事务写在这 ├── mapper // MyBatis-Plus Mapper ├── entity // 实体类 ├── config // 拦截器、跨域、分页插件配置 ├── common // 统一返回、异常处理 └── RentalApplication.javapom.xml里核心依赖就这几组dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependencyapplication.yml里有两个容易出错的点数据源url要带serverTimezoneAsia/Shanghai否则时间会差8小时MyBatis-Plus要开启下划线转驼峰字段映射才对得上spring: datasource: url: jdbc:mysql://localhost:3306/rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 redis: host: localhost port: 6379 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto分页用MyBatis-Plus内置插件但有个前提必须把拦截器注册成Bean否则分页查询不会真正分页而是把全表数据查出来再内存分页。这个坑我见过不止一次。3.2 登录鉴权JWT加拦截器简单但要把原理讲透选JWT而不选Session最核心的理由是它无状态。Session需要服务端存储前后端分离时还要处理跨域和集群共享问题JWT把用户信息签在Token里服务端只需要验签天然适合接口化架构。JwtUtil的代码很简单但要注意过期时间和签名算法Component public class JwtUtil { Value(${jwt.secret}) private String secret; public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }然后写一个拦截器思路很直接登录和注册接口放行其余接口从Header取Token解析失败就返回401。注意拦截器里不要查数据库Token里已经有userId和username需要更多信息再在Controller里查。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行登录注册接口的逻辑写在这里 String token request.getHeader(Authorization); // 解析失败返回401解析成功把userId塞进request属性 return true; } }注册的地方也别忘registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/**);如果你想做得更完善可以把退出登录的Token放进Redis黑名单设置和Token一样的过期时间。这个设计在答辩里很能说明问题JWT无状态的好处讲完再补充一个“用Redis解决Token主动失效”的方案说明你不是只会背概念。3.3 下单接口事务、行锁、冲突校验一个都不能少这是整个系统最值得讲清楚的地方。两个用户可能同时看到同一辆车可租然后同时下单如果不加控制数据库里就会出现两条重叠订单。解决的思路是三层配合事务保证原子性行锁保证并发互斥冲突校验保证业务合法。核心代码长这样Transactional(rollbackFor Exception.class) public RentOrder createOrder(CreateOrderDTO dto, Long userId) { // 1. 校验参数 if (dto.getStartTime().isAfter(dto.getEndTime())) { throw new BusinessException(起租时间不能晚于预计还车时间); } // 2. 锁定车辆行防止并发下单 Car car carMapper.selectByIdForUpdate(dto.getCarId()); if (car null || car.getStatus() ! 0) { throw new BusinessException(该车辆当前不可租); } // 3. 检查目标时段是否冲突 int count rentOrderMapper.countConflict(car.getId(), dto.getStartTime(), dto.getEndTime()); if (count 0) { throw new BusinessException(该时段车辆已被预订); } // 4. 占用车辆 carMapper.updateStatus(car.getId(), 0, 1); // 5. 生成订单 RentOrder order new RentOrder(); order.setOrderNo(generateOrderNo(userId)); order.setUserId(userId); order.setCarId(car.getId()); order.setStartTime(dto.getStartTime()); order.setEndTime(dto.getEndTime()); order.setStatus(0); rentOrderMapper.insert(order); return order; }selectByIdForUpdate在Mapper里这么写Select(SELECT * FROM car WHERE id #{id} FOR UPDATE) Car selectByIdForUpdate(Param(id) Long id);为什么顺序不能乱因为必须先锁行再做冲突校验最后更新状态。如果先做冲突校验再锁行两个并发请求可能同时读到“没有冲突”然后同时通过。行锁生效还有一个前提查询必须走主键索引而且事务不能提前提交。如果走全表扫描InnoDB可能把锁升级成表锁并发能力就废了。这个细节面试官和答辩老师都爱问答出来很加分。3.4 还车结算与超时关单还车时核心是金额计算。实际用车时长按分钟算再乘以每小时租金。这里必须用BigDecimal后端金额计算用浮点数会出精度问题Transactional(rollbackFor Exception.class) public void returnCar(Long orderId, Long userId) { RentOrder order rentOrderMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId)) { throw new BusinessException(订单不存在); } if (order.getStatus() ! 1) { throw new BusinessException(当前状态不可还车); } Car car carMapper.selectById(order.getCarId()); LocalDateTime now LocalDateTime.now(); long minutes Duration.between(order.getStartTime(), now).toMinutes(); BigDecimal amount car.getPricePerHour() .multiply(BigDecimal.valueOf(minutes)) .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); order.setActualEndTime(now); order.setTotalAmount(amount); order.setStatus(2); rentOrderMapper.updateById(order); carMapper.updateStatus(car.getId(), 1, 0); }超时未取车自动关单我建议用Spring自带的定时任务。每五分钟扫一次订单表把“开始时间已过30分钟且仍为待取车”的订单关闭同时恢复车辆状态。注意定时任务和用户手动操作可能同时发生所以关单要用条件更新防止覆盖用户操作UPDATE rent_order SET status 4 WHERE id #{orderId} AND status 0受影响行数为0时说明订单状态已经被其他操作改掉定时任务直接跳过就行。这种“条件更新”是并发场景防覆盖的常用手法写进论文也算一个技术亮点。3.5 接口设计与前后端联调接口设计直接决定前端工作量尽量按资源设计语义清晰。方法路径说明POST/api/auth/register用户注册POST/api/auth/login用户登录GET/api/cars车辆列表支持关键词和分页POST/api/orders创建订单POST/api/orders/{id}/pickup取车POST/api/orders/{id}/return还车GET/api/orders/my我的订单列表同时做一个统一返回体不管成功失败都给前端一个固定结构比如{code, message, data}前端只判断code。异常用全局异常处理器捕获不要让系统默认的错误页直接暴露给用户。前后端分离联调时跨域问题几乎必现。SpringBoot里加一个CorsFilter配置就好。容易漏的是credentials和AllowedOriginPatterns两处不加的话浏览器会把跨域请求拦截掉前端调试半天找不到原因。4. 避坑记录这些问题我几乎每次都会遇到4.1 事务不生效的三种典型场景第一个场景是同类内部调用。比如service里createOrder方法调用了同一个类的updateStatus方法updateStatus上的Transactional不会生效。因为Spring事务是通过容器生成增强对象实现的同类内部自调用不会经过增强层事务注解形同虚设。解决办法是把被调用的方法放到另一个Service里或者注入自身。第二个场景是异常被catch吞掉。事务默认只在遇到RuntimeException时回滚如果方法里catch住异常不往外抛事务不会回滚数据库里就会残留半成品数据。第三个场景是方法不是public。Spring AOP无法对非public方法做增强这个坑比较隐蔽因为代码能编译能运行但事务就是不生效。排查时先看调用链再看异常处理最后看方法修饰符基本能定位90%的问题。4.2 时间字段在前后端之间的“变形记”时间是最容易出幺蛾子的地方。LocalDateTime配合Jackson序列化默认输出的是数组格式前端根本没法直接用。解决办法是在字段上加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime startTime;另外MySQL连接串里的serverTimezone不设置存进去的时间可能差8小时。前端那边也有一坑JS的new Date(2025-06-01 12:00:00)在部分浏览器里直接显示Invalid Date所以前端日期组件提交的格式必须跟后端约定好统一用yyyy-MM-dd HH:mm:ss。4.3 金额计算double的0.1加0.2问题任何涉及金额的计算double都有精度坑。0.1加0.2在二进制浮点里不是0.3而是0.30000000000000004。租赁系统还涉及乘法除法误差会被放大。所以单价、时长、总价全部用BigDecimal计算时还要指定RoundingMode避免除不尽的情况。4.4 订单取消以后车辆状态没被回收很多人写完下单和还车就把取消功能草草带过。结果用户取消订单后车辆状态还卡在“已租”其他用户永远租不了这辆车。取消的逻辑必须和还车一样在同一事务内完成两件事订单状态改为已取消车辆状态恢复为可租Transactional(rollbackFor Exception.class) public void cancelOrder(Long orderId, Long userId) { RentOrder order rentOrderMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId)) { throw new BusinessException(订单不存在); } if (order.getStatus() ! 0) { throw new BusinessException(当前状态不可取消); } rentOrderMapper.cancelOrder(orderId, 0, 3); carMapper.updateStatus(order.getCarId(), 1, 0); }4.5 统计查询里的几个隐藏坑做管理端统计时最容易翻车的是近7天订单趋势。SQL按天GROUP BY之后没有订单的日期压根不会出现在结果里折线图就缺了好几个点。需要在Java侧组装一个连续的日期列表再把查询结果填进去没有的补零。金额统计要记得排除已取消和超时关单的订单否则管理端看到的“营收”虚高。而且分页查询不配置分页插件的话数据量一大就会出现严重的性能问题页面越翻越慢。把这些问题整理成一个速查表开发时按图索骥现象根本原因解决方案事务没回滚同类内部方法自调用拆Service或注入自身前端时间少8小时或格式乱时区与序列化未配置统一LocalDateTime加格式注解金额合计精度丢失用了double计算全部改用BigDecimal取消订单后车仍显示已租状态更新不完整同事务回收车辆状态统计折线图缺日期SQL没有补零Java侧组装连续日期5. 答辩与演示把“会做”变成“会讲”5.1 演示脚本按业务闭环走别跳答辩演示时间通常只有五到十分钟脚本一定要提前设计好别现场临场发挥。我的建议是按业务闭环走顺序如下登录用户端进入车辆列表展示筛选和车辆状态。选一辆车下单故意选一个会产生冲突的时间段展示系统弹出“该时段已被预订”的提示。这一手能证明你不是摆设校验是真的。正常选择一个不冲突的时间下单在“我的订单”里看到待取车状态。点取车订单变为用车中再回到车辆列表这辆车状态已经变成已租。点还车展示费用明细比如用车1小时20分钟怎么从分钟数算成金额的。切到管理端展示订单追踪、车辆管理、统计图表。演示前一定要准备好数据和账号现场注册、现场造数据是演示事故的高发区。5.2 高频提问和应答思路答辩老师问的问题其实高度集中提前准备好应答思路就不慌提问方向应答要点为什么用JWT不用Session无状态、跨端友好、集群部署不需要共享存储补充Redis黑名单解决Token失效问题怎么防止同一辆车同一时间被重复下单InnoDB行锁FOR UPDATE加时间重叠校验事务保证一致性按代码顺序讲车辆状态和订单状态会不会不一致所有状态变更收敛在事务内取消和超时都走同一套回收逻辑系统怎么扩展成真正的共享汽车平台增加定位、车辆开关锁、分时计价接口核心订单模型和状态机不用动答辩最忌讳的是只会念代码。你把每个设计决策背后的“为什么”讲清楚哪怕实现上有一些简化老师也不会深挖。尤其并发控制和时间重叠这两个点能主动画图讲明白这一部分基本就稳了。最后聊一点我自己的感受。这个题目我前前后后帮着落地过好几轮最大的心得不是某段代码多精妙而是先把状态流转画对再动手。很多同学做管理系统花最多时间在页面上结果业务逻辑一问就露馅。汽车租赁系统真正的门槛恰恰在状态机、并发和结算这三件事上你把这三个点想透了代码只是把它们翻译出来而已。如果你时间来得及建议在答辩前把超时关单和时间冲突的演示各练一遍这两个场景能讲清楚这门设计基本就站得住脚了。