SpringBoot汽车租赁系统源码解析:订单状态机与权限控制实战 📅 发布时间:2026/9/11 15:21:03 👁 浏览次数: 简介面向毕业设计或Java全栈开发学习者的完整项目资料包以SpringBoot为核心实现汽车租赁管理系统涵盖前台展示与后台管理两大模块包括汽车信息、资讯、论坛、公告、留言板以及用户管理、汽车类别管理、租车订单管理等核心功能能为课程设计或论文写作提供可直接复用的工程参考。包内共6个文件以源码压缩包为主还包括sql数据库脚本、doc/docx设计文档、ppt答辩演示文稿和txt说明文件整体约32.3MB。其中数据库脚本可快速建表文档介绍系统设计与功能模块PPT适合毕业答辩展示。目前已有42人学习浏览资源结构清晰、开箱即用适合需要完成类似课题或深入理解SpringBoot业务系统开发的读者。1. 汽车租赁系统拆解为什么值得把这份SpringBoot源码跑通拿到一个SpringBoot汽车租赁管理系统的源码包先别急着点启动。我拆过的毕设项目里这份属于典型的中型业务系统双角色、前后台分离、订单状态多、管理表格多。它的核心不是上下游微服务而是把管理员-车辆-订单-用户这条主流程做扎实。如果只想交一份毕业设计那你更需要的是把租车订单的取消逻辑和权限控制讲成面试能答出来的话如果想复用到真实项目这个结构可以直接改造成会议室预约、工单派单。适合的人群是准备毕设答辩的在校生以及想找个完整实例梳理SpringBoot整合数据库和订单业务流的初级后端开发。2. 数据库设计先从db.sql看起租车主表和状态字段是怎么埋进去的数据库是这类系统最容易拿分也最容易露怯的地方。docx文档和数据库文档里会把表结构讲清楚但真正落地的是db.sql那一段建表语句。先把MySQL 5.7或8.0建好然后直接导入脚本。导入命令不要用图形工具拖拽避免字符集不一致导致中文乱码。mysql -h127.0.0.1 -uroot -p --default-character-setutf8mb4 -e source /path/to/db.sql上面的命令会把表的默认字符集锁在utf8mb4这样汽车品牌、车型描述、论坛帖子和留言板里的中文与Emoji都不会变成问号。如果是用Navicat导入也要把编码选成65001UTF-8否则后面查数据时到处是乱码。导入完成之后重点不是数表数量而是看字段类型和逻辑外键。表结构大致是按基础资料-业务单据-交互内容三层组织的下图能省去翻文档的时间直接对照表名看状态字段。表名业务含义关键状态字段users前台用户status0禁用1正常admin管理员role_level1超级2普通car_category汽车类别无car_info汽车信息car_status0下架1上架car_order租车订单order_status0待支付1已支付2已取车3已还车4已取消cancel_order取消订单记录无car_news汽车资讯无car_forum汽车论坛帖status0待审核1通过2驳回car_notice公告信息无car_message留言板status0待审核1展示每一张表都带主键id、创建时间create_time和修改时间update_time这种标准字段在写新增接口时能省很多事。真正要花时间看的是users、admin、car_order三张表的状态字段设计。2.1 用户表与管理员表角色分离的常见做法用户和管理员独立建表而不是共用一张表加一个role字段。这在毕设里很常见因为后台接口的权限可以直接用admin_id判断前台的登录则走user_id两个会话互不干扰也方便在普通管理员之外再套一层超级管理员。管理员表里一般会有role_level字段数字越小权限越大。建表的时候普通管理员管理功能实际上就是对这个表做增删改查。下面是我从项目里抽取出来的等价建表逻辑CREATE TABLE admin ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(255) NOT NULL COMMENT BCrypt后的密码, real_name varchar(50) DEFAULT NULL COMMENT 姓名, role_level tinyint(4) DEFAULT 2 COMMENT 1超级管理员 2普通管理员, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员表;注意我在这里给username加了唯一索引防止后台创建普通管理员时重复。密码字段如果是255位多半是用了BCrypt老项目也可能是32位MD5。拿到源码要先确认配置文件里的密码策略否则自己注册新管理员的密码在登录时永远校验不过。用户表相比更细一些通常包含姓名、手机号、身份证号、驾驶证号和状态status管理员在用户管理里修改这个状态被禁用的用户不能下单。2.2 汽车信息表与订单表价格、库存和订单状态的联动汽车信息表是前台展示和后台管理的交集。几个关键字段是品牌brand、型号model、日租价格daily_price、汽车图片image_url、剩余库存stock、状态car_status。stock这个字段很容易被忽略但它决定了用户下单时能不能继续租。如果汽车下架car_status变成0前台列表应该过滤掉它但后台仍然能看到并操作这是前后台数据一致性的基础。订单表是整张业务的地基。核心状态在order_status字段不应使用字符串而是用tinyint存数值然后在Java枚举里定义含义。数据库里同时会有order_no字段唯一索引保证订单号不重复。带cancel_time或return_time的订单表在取消和还车时还要回写时间字段。一个常见误区是在取消订单时直接把记录删掉这样后台的取消订单管理模块就没有数据可看也丢掉了审计价值。查询订单列表时最简单有效的方式是关联用户表和汽车表一次拿到显示所需的信息SELECT o.id, o.order_no, u.username, c.brand, c.model, o.rental_days, o.total_price, o.order_status, o.create_time FROM car_order o LEFT JOIN users u ON o.user_id u.id LEFT JOIN car_info c ON o.car_id c.id ORDER BY o.create_time DESC这里用LEFT JOIN而不是INNER JOIN是因为有些测试数据里用户或汽车可能被删除如果改成INNER JOIN历史订单列表会少行。后台管理页要看的就是这些列订单详情页再单独查明细。把这条SQL交给MyBatis的Mapper返回结果应该是一个OrderVO而不是实体类这是值得养成的习惯查询结果不要直接映射底层表实体避免把password、库存这类字段带到前端。订单号的生成不建议用数据库自增id直接暴露给用户后台管理系统很容易遍历出某段时间内的所有订单。常见做法是在Java服务里用时间戳加随机数生成20位以内的字符串再配合唯一索引兜底public String generateOrderNo() { SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); String timePart sdf.format(new Date()); int randomPart (int)((Math.random() * 9 1) * 100000); return timePart String.valueOf(randomPart); }这段代码的时间部分精确到秒随机部分是6位数字一共20位已经足够应付毕设级并发。如果要在真实业务里用就把随机部分换成Redis自增号避免同一秒内生成重复订单号。uk_order_no唯一索引是最后一道防线一旦碰撞保存时抛出DuplicateKeyException再做重试即可。2.3 资讯、论坛、公告与留言板通用内容表的两种设计汽车资讯、汽车论坛、公告信息、留言板这四类功能本质上都是内容发布与展示。很多毕业设计会给每类建一张独立表字段大同小异都是标题、内容、发布时间、发布人id。另一种做法是共用一张content表用type字段区分但那样查询时会多一道过滤条件分页和排序也要各自写。既然系统数据库文档里是按模块分表顺着原设计走就好以后扩展字段也不会影响其他模块的展示。论坛和留言板有一点特殊用户输入的内容不可控所以这两张表需要带审核状态字段。前台只查询status为1的数据管理员在后台查看所有待审核内容。这里的关键点是把展示状态和实际状态分开。汽车信息表里的car_status是管理员手动控制的上下架库存则是订单取消后要回写的业务数值。区分好语义后面写状态机才不容易混。汽车类别表只是给汽车信息表增加一个category_id逻辑外键前台列表拿category_id关联类别名称筛选时仍然用id做条件。3. 租车订单状态机与权限控制把取消逻辑写清楚第2章把表结构落地了这一章从代码层面看订单数据和权限。SpringBoot项目的标准结构是controller-service-mapper三层如果源码里引入了MyBatis-Plus那Service继承ServiceImpl之后很多基础增删改查方法可以直接用不用手写SQL。先给结论租车订单的状态机应该在Service层做而不是在Controller里堆if-else。这样后续增加状态时只需要改枚举和Service里的迁移逻辑Controller和前端接口保持稳定。最简单的订单状态机只有五个状态待支付、已支付、已取车、已还车、已取消。每个状态允许进入下一个状态是固定的。用户手动取消只发生在待支付和已支付阶段已支付阶段取消订单要回写车辆库存说明车辆又可租了待支付阶段取消不改变库存因为尚未真正锁定库存。3.1 用枚举管理订单状态而不是魔法数字先定义一个枚举类OrderStatus把状态编码、描述集中在一起。数据库里只存code对应的int值前端拿code再翻译成文本。public enum OrderStatus { UNPAID(0, 待支付), PAID(1, 已支付), PICKED_UP(2, 已取车), RETURNED(3, 已还车), CANCELLED(4, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }这样在Service里写判断时非常清晰。比如判断订单是否能取消状态是UNPAID或PAID时可以如果已经取车或还车就不能取消。枚举里不要放数据库字段以外的东西desc尽量不要存进数据库否则以后改文案还需要同步数据库。编码状态可进入的下一状态0待支付已支付、已取消1已支付已取车、已取消2已取车已还车3已还车无4已取消无这张表就是你写代码时的验收清单。如果发现源码里某个状态能跳过两个节点那一定是有逻辑漏洞比如未取车就完成还车这种单子在库存盘点时会出问题。3.2 取消订单时如何处理库存与取消原因取消订单管理模块不仅要更新订单状态还要在cancel_order表里记录取消原因。可以把取消原因作为请求参数传到Controller再在Service里做事务操作。下面是Service层的核心方法Transactional(rollbackFor Exception.class) public boolean cancelOrder(OrderCancelRequest request) { CarOrder order getById(request.getOrderId()); if (order null) { throw new BusinessException(订单不存在); } Integer status order.getOrderStatus(); if (status OrderStatus.PICKED_UP.getCode() || status OrderStatus.RETURNED.getCode()) { throw new BusinessException(当前订单状态不可取消); } order.setOrderStatus(OrderStatus.CANCELLED.getCode()); order.setCancelTime(new Date()); updateById(order); CarInfo car carInfoService.getById(order.getCarId()); if (car ! null) { car.setStock(car.getStock() 1); carInfoService.updateById(car); } CancelOrder cancelOrder new CancelOrder(); cancelOrder.setOrderId(order.getId()); cancelOrder.setUserId(order.getUserId()); cancelOrder.setCancelReason(request.getCancelReason()); cancelOrder.setCreateTime(new Date()); cancelOrderService.save(cancelOrder); return true; }这段代码的关键点是Transactional事务注解。第3步更新订单、第4步回写库存、第5步保存取消原因三个动作要么全部成功要么全部回滚。最容易踩的坑是把事务加在Controller上或者加在同一个类内部调用的方法上那样Spring的代理不会生效库存和订单状态会出现不一致。补充一个容易忽略的参数request里如果允许用户填取消原因一定要做长度校验和空值处理。表单提交时限制最多200字后端再用Size(max 200)挡一道否则超长文本插入数据库会报错。真正的租车系统里取消原因还要区分用户主动取消和管理员强制取消可以在请求参数里增加一个source字段不过在毕业设计里统一走用户侧取消就行。3.3 前后台权限如何隔离用拦截器还是Spring Security这类管理系统通常会采用Spring Security或者自定义拦截器做登录拦截只有管理员能访问后台接口普通用户只能访问前台接口。更简单的做法是写一个HandlerInterceptor拦截/admin/**路径判断session中是否有管理员信息拦截/user/**路径判断是否有用户信息。建议优先看源码里是否引入了spring-boot-starter-security如果没有就用拦截器方案改动最小。下面是拦截器注册的配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AdminAuthInterceptor()) .addPathPatterns(/admin/**) .excludePathPatterns(/admin/login); registry.addInterceptor(new UserAuthInterceptor()) .addPathPatterns(/user/**) .excludePathPatterns(/user/login, /user/register); } }拦截器里取session或header里的token按角色判断。如果项目是前后端分离前端会在请求头放token拦截器要改成解析token并从Redis或数据库校验登录态。无论哪种方式一定要先放行登录和注册接口否则还没登录就被拦截页面永远进不去。如果想看更细粒度的权限比如普通管理员不能删除用户可以引入Spring Security的PreAuthorize注解在启动类上加EnableGlobalMethodSecurity(prePostEnabled true)即可。这里给一个用户查询订单列表的接口示例RestController RequestMapping(/user/order) public class UserOrderController { Autowired private OrderService orderService; GetMapping(/list) public Result listOrder(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { Long userId UserContext.getUserId(); PageOrderVO page orderService.listByUser(userId, pageNum, pageSize); return Result.success(page); } }上面代码里的UserContext是一个ThreadLocal工具类登录成功后把userId放进去请求结束前清掉。Service里不用到处透传userId权限过滤集中在同一处。参数pageNum和pageSize分别控制页码和每页条数默认值设定为1和10配合前端表格组件能避免空指针。如果项目用MyBatis-Plus分页可以直接用Page类不需要手写limit。手写分页SQL时如果拼接条件不当容易产生SQL注入风险统一用QueryWrapper则更安全。4. 前台展示与后台管理如何通过接口联动订单流和权限解决之后剩下的是前台页面数据从哪里来。汽车信息、资讯、论坛、公告、留言板看似功能多本质都是列表查询加状态过滤。后台管理员维护数据前台游客或用户读取展示。控制层返回JSON前端Vue或Thymeleaf负责渲染。如果源码里没有独立前端工程那就是SpringBoot模板引擎直接输出页面接口设计思路不变。4.1 汽车信息列表的后端分页接口前台首页的汽车信息展示需要一个分页接口支持类别筛选和关键词搜索。最常见的参数是pageNum、pageSize、categoryId、keyword。categoryId不传表示全部类别keyword模糊匹配品牌或车型。GetMapping(/api/car/list) public Result carList(RequestParam(required false) Long categoryId, RequestParam(required false) String keyword, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 8) Integer pageSize) { LambdaQueryWrapperCarInfo wrapper new LambdaQueryWrapper(); wrapper.eq(CarInfo::getCarStatus, 1); if (categoryId ! null) { wrapper.eq(CarInfo::getCategoryId, categoryId); } if (StringUtils.hasText(keyword)) { wrapper.and(q - q.like(CarInfo::getBrand, keyword) .or().like(CarInfo::getModel, keyword)); } wrapper.orderByDesc(CarInfo::getCreateTime); PageCarInfo page carInfoService.page(new Page(pageNum, pageSize), wrapper); return Result.success(page); }参数说明categoryId为空字符串或null时都不进入条件keyword同理。这里用LambdaQueryWrapper而不是手写注解SQL好处是列名在编译期就能检查数据库改列名后不会等到运行时才报错。pageSize默认8适合前台卡片式布局。如果后台要复用这个逻辑把carStatus条件换成不限制即可或者单独写一个AdminController接口。前台与后台共用同一张car_info表但要区分可见和可管理。前台只取carStatus为1的数据后台列表则显示全部。很多同学把状态判断直接写在Service里导致后台也看不到下架车辆这是一个常见的边界错误。正确做法是在Controller层或查询条件层区分而不是改Service的公共逻辑。4.2 后台管理表格与订单状态更新后台的汽车信息管理、订单管理、用户管理本质是几张表格的增删改查操作。汽车的新增和编辑可以共用一个保存接口用id是否为null判断是新增还是更新。订单状态更新则要单独设计因为订单状态不能随意修改只能按照状态机规则迁移。方法路径角色说明GET/api/car/list游客/用户前台汽车分页列表POST/user/order/add用户创建租车订单GET/user/order/list用户当前用户订单POST/admin/order/update管理员处理订单状态POST/admin/car/save管理员新增或编辑汽车GET/api/forum/list游客/用户已审核论坛帖列表POST/admin/forum/audit管理员审核通过或驳回订单更新接口的请求参数不需要太多一个orderId加一个目标状态值就够了。Service里拿到目标状态后先校验当前状态是否允许迁移再执行更新。比如管理员操作订单从已支付改为已取车时日期字段也要同步设置pickupTime。千万不要把整个订单对象提交到后端做全字段更新那样用户字段和价格字段会被任意覆盖。论坛和留言板的后台审核类似列表接口显示status为0的记录管理员点击通过就把status改成1点击驳回改成2。这里需要考虑驳回后用户端是否显示原因如果系统没有驳回原因字段可以在公告或站内信里说明。大多数毕业设计的实现里驳回只是隐藏内容没有进一步通知机制这一步做到位就已经合格了。4.3 数据库字段映射与VO隔离前台页面拿到的是记录列表但汽车信息表里存的是categoryId页面要显示类别名称这需要VO对象拼接。可以在SQL里使用LEFT JOIN把类别名称查出来也可以在Service里循环查类别表。后者虽然方便但在列表数据量大的时候会产生N1查询问题100条汽车数据就要多100次查询。这块优化值得做一下。我一般会写一个CarVO继承或包装CarInfo实体额外加categoryName字段。Mapper里用resultMap做映射或者直接在XML里写JOIN查询返回VO。SpringBoot项目的原则是Controller返回给前端的对象不要直接用数据库实体避免把库存数字、上下架状态等内部字段暴露给用户。前台首页还需要展示汽车资讯、公告信息、论坛热帖三个板块。这三个板块各自独立接口参数只需要pageNum和pageSize最多再加一个排序字段。资讯和公告状态比较简单只要发布就能展示不需要审核。论坛热帖则要加一个浏览数或回帖数字段做排序数据库文档里如果没有这个字段可以临时用create_time倒序替代。5. 部署排错中最容易被坑的三个地方车跑不起来80%的问题集中在数据库配置和字符集剩下20%是时间格式。看到报错就先去翻application.yml把数据源配置和实际MySQL版本对齐。不要急着百度先看错误日志里的关键行。5.1 MySQL驱动与时区问题如果源码是Spring Boot 2.3默认用的是5.1.46驱动连接MySQL 8.0会报ClassNotFoundException或时区错误。解决方法是把驱动改成mysql-connector-java 8.0.x并检查连接串里的serverTimezone参数。以root连接MySQL 8.0时还要确认mysql.user表中的plugin是caching_sha2_password否则会出现Public Key Retrieval is not allowed错误在jdbcUrl后面加上allowPublicKeyRetrievaltrue即可。spring: datasource: url: jdbc:mysql://127.0.0.1:3306/car_rental?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezone为什么重要MySQL 8.0默认时区是UTC而本地是中国标准时间如果没设置程序里查询出来的日期会比实际时间晚8个小时。早上10点下单数据库里显示凌晨2点前端表格看起来非常奇怪。设置成Asia/Shanghai后时间戳和本地时间一致。driver-class-name也要改成com.mysql.cj.jdbc.Driver这是8.0的新驱动类。5.2 数据库编码导致的中文乱码db.sql导入后如果建立数据库时没有指定utf8mb4那中文字段写入后是一个个问号。排查时先看建库语句CREATE DATABASE IF NOT EXISTS car_rental DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;如果已经建好了可以用ALTER DATABASE和ALTER TABLE补上。另一个隐蔽点是连接字符串里characterEncodingutf8mb4是否写正确注意不是utf-8中间这个连字符在数据库URL里会出错。检查完这些重启项目再插入一条中文数据如果还乱码就排除是Linux系统LANG环境变量问题设置export LANGzh_CN.UTF-8后再启动Java进程。5.3 全局日期格式化让前端表格少踩坑后台订单管理页面要显示创建时间、取车时间、还车时间最怕返回的JSON是2025-01-01T12:00:00这种UTC格式。在application.yml里配置Jackson全局时间格式一劳永逸spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai配置之后后端所有Date字段返回给浏览器时都统一为本地时间字符串。但要注意如果项目中有的字段是LocalDateTime类型需要额外引入jackson-datatype-jsr310模块并加上JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。前端表格拿到字符串后直接渲染即可不需要再做toLocaleString转换。最后一个实用技巧是查看SQL日志。在application.yml里加上mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl启动后控制台会打印每一条SQL和参数。排查订单状态更新是否生效、库存回写是否执行时直接看SQL输出比加日志断点更直观。确认后删掉这个配置避免生产环境输出过多日志。本文还有配套的精品资源点击获取