多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SpringBoot+Vue汽车租赁管理系统:从订单状态机到权限控制全解析

SpringBoot+Vue汽车租赁管理系统:从订单状态机到权限控制全解析 SpringBootVue 做汽车租赁管理系统这个组合在毕设里算是一套“标准答案”了。项目本身不复杂但前后端该有的东西都有用户登录、权限区分、车辆管理、订单流转、租金计算、统计图表一个完整的业务闭环。如果你是本科毕业设计或者课程设计选了这么个题目只要把下面这些逻辑理清楚代码写得干净一点答辩的时候能解释清楚“为什么这么设计”拿个不错的成绩问题不大。这篇文章我不打算只贴一个“源码下载地址”然后就完事而是把我当年做这个项目时踩过的坑、想明白的道理、以及这个系统背后真正值得你花时间的核心逻辑都摊开来讲。不管你拿到的是哪一版源码只要你理解了这套业务和设计思路换成别的技术栈比如把前端换成 React或者把 MySQL 换成 PostgreSQL你照样能做得出来。1. 项目概述这到底是个什么样的系统1.1 核心功能与业务场景汽车租赁管理系统解决的是一个很简单但很繁琐的问题一个租车公司手里有几十台车怎样才能搞清楚每台车现在在哪、被谁租着、租金该收多少、什么时候该保养了。往细了说系统至少要管住这几件事车辆信息管理车的基本资料品牌、型号、车牌、颜色、座位数、车辆状态可用、已租、维修、保养、照片、每日租金定价。客户信息管理租车的人是谁身份证号、电话、驾驶证信息租车历史记录。订单流程从客户下单、选车、提交订单到管理员审核、确认取车再到实际还车、结算租金最后归档。这个流程是系统的核心主线。租金计算租金不是简单的“日租价乘以天数”否则这套系统就没什么技术含量了。真实场景里通常会涉及租期不足一天的怎么算、超时还车怎么加收费用、押金怎么核算退款、优惠折扣怎么叠加。统计报表每个月租出去了多少车、营收是多少、哪款车最受欢迎、哪个客户是回头客。毕设里做几个 ECharts 图表展示出来视觉上就直接加印象分。这个系统在业务上和你手机里那些共享单车 App 有本质区别共享单车是全自动的计费而汽车租赁因为涉及证件审核、押金、车况确认、事故责任归属线下的“人”参与度非常高。所以这个系统的核心并不是“自动化”而是把线下流程里的每一个环节都数字化、可追溯。这一点想明白了你在设计表和状态机的时候就不会画错方向。1.2 为什么这套选题适合毕设和课设很多人选毕设题目有个误区要么选了一个“看起来很简单但全是什么管理系统增删改查”的题答辩的时候没什么可讲的要么选了一个“太硬核”的题比如做一个分布式高并发秒杀系统结果自己根本写不完代码全是拼接的连自己都看不懂答辩一问就露馅。汽车租赁管理系统属于典型的“业务复杂度适中、技术覆盖面广”的选题。它不是一个空壳的增删改查它有明确的核心流程订单状态流转有需要认真思考的设计点租金计算规则、权限控制也有可以拔高的技术点统计分析、Redis 缓存、文件上传、Excel 导出。难度你可以自己调节只是六张表互相增删改查也能跑但如果你愿意把一个订单状态机设计得很严谨把数据权限做得很细致这也能成为你在答辩时最有说服力的亮点。另外这个系统天然带有多角色。管理员、普通用户前台租车、门店操作员不同的角色看到不同的页面、操作不同的功能。这就逼着你必须去考虑权限问题——不是你假装做一个判断就能糊弄过去的。这个设计点在任何答辩评委那里都吃得开。2. 技术选型背后的事SpringBoot Vue MySQL 为什么是黄金三角2.1 后端为什么选 SpringBootSpringBoot 在今天已经是 Java Web 开发的事实标准了不用争论。它对刚接触企业级开发的在校生来说最大的意义是“帮你把繁琐的配置收走”。如果你用过老式的 Spring MVC 项目你会记得那种痛苦XML 配置写了一堆web.xml、spring-mvc.xml、数据源配置、扫描包配置光是把项目跑起来就累得半死而这还没有写一行业务代码。SpringBoot 用自动配置把这些全都解决了你只需要在application.yml里写好数据库连接、端口号然后就能专注于写 Controller、Service、Mapper。这个项目我建议你选择 SpringBoot 2.7.x 版本而不是 3.x。原因是社区里大量的教程、开源代码都是基于 2.x 的而且 2.x 和之后要整合的很多中间件兼容性更好。SpringBoot 3.x 强制要求 JDK 17且底层使用 Jakarta EE 命名空间很多旧工具类直接不兼容对于只想顺利毕业的学生来说没必要在这种地方给自己挖坑。启动类写得非常简洁SpringBootApplication MapperScan(com.rental.system.mapper) public class CarRentalApplication { public static void main(String[] args) { SpringApplication.run(CarRentalApplication.class, args); } }三行代码一个可启动的 Web 服务这在十年前是不敢想象的。2.2 前端为什么选 VueVue 在国内中小型项目里的普及率非常高学习曲线比 React 缓得多特别适合“从零开始且需要快速出效果”的前端项目。Vue 的核心价值在于“响应式数据绑定”和“组件化开发”。比如车列表页面你只需要维护一个carList数组在 Vue 里把它v-for循环渲染到页面上当你从后端拿到数据更新carList时页面自动刷新。这在原生 JavaScript 里需要手动操作 DOM写一大堆 appendChild而且一旦业务复杂代码就失控了。这个项目的前端我建议直接用 Vue CLI 创建或者 Vite配合以下几个关键库Vue Router管理页面路由。比如/car/list是列表页/order/detail/:id是订单详情页。还有动态路由的概念根据用户角色动态生成菜单。Axios发送 HTTP 请求到后端接口统一拦截处理 token 和错误信息。Element UI或 Element Plus组件库。表格、表单、日期选择器、弹窗这些都直接用现成的千万别自己手写样式你会浪费大量时间。2.3 MySQL 为什么合适MySQL 在关系型数据库里的地位不用多说。对这个项目来说MySQL 8.0 是首选。数据表之间的关系用户与订单、订单与车辆、车辆与品牌非常清晰用外键关联和索引就能拿到理想的查询性能。有人问我为什么不选 Oracle因为没必要而且毕设环境里 Oracle 的安装就够折磨人了。为什么不选 MongoDB因为你这个业务根本不需要文档型数据库的灵活性车辆、订单有明确的结构和约束关系型数据库是天然合适的。2.4 为什么坚持前后端分离这个项目的架构是前后端完全分离的Vue 开发环境跑在 8080 端口SpringBoot 跑在 8081 端口两者通过 RESTful API 通信期间用 JSON 传递数据。这样做最大的好处是结构清晰、职责单一。后端的 Controller 只负责处理请求返回 JSON前端只用关心渲染。两个人在分工协作时比如你和你的组员一个人管前端一个人管后端互不干扰只需要提前约定好接口文档。另一个实际的好处是你可以用其他工具直接调试后端的接口比如 Swagger、Postman、Apifox。这一点在联调阶段能救你的命。3. 项目目录结构一开始先定规矩后面才不会乱3.1 后端目录结构一个清晰的项目结构在答辩时能让你少费很多口舌。我建议按“经典分层”来组织com.rental.system ├── config // 配置类Cors、Swagger、Jwt等 ├── controller // 控制器接收请求 ├── service // 业务逻辑层接口 实现 ├── mapper // 数据访问层MyBatis 的 Mapper 接口 ├── entity // 实体类 ├── vo // 视图对象给前端返回的具体结构 ├── dto // 数据传输对象接收前端的参数 ├── common // 统一返回结果、异常处理、常量 ├── utils // 工具类 └── CarRentalApplication.java这个结构的核心原则只有一个Controller 里不要写业务逻辑。Controller 拿到前端参数以后直接转给 Service 处理Service 内部把事情做完返回一个结果Controller 再包装成统一的 JSON 格式交给前端。这个习惯你从学生阶段养成了以后到公司里没人会说你写的代码“像个学生”。3.2 前端目录结构Vue 项目结构也很有讲究src ├── api // 接口封装按模块拆分 ├── assets // 静态资源 ├── components // 公共组件上传组件、表格组件 ├── router // 路由配置 ├── store // Vuex 状态管理 ├── views // 页面组件视图 ├── utils // 工具类axios 封装等 ├── App.vue └── main.js这里我想特别说一下api目录。很多学生习惯在页面里直接写this.$http.post(/api/login)看起来方便但一旦接口地址变了或者后端结构变了你就需要去每一个页面里改极其痛苦。正确做法是每个模块的接口统一封装在api目录下的一个 JS 文件里页面里只调用方法。比如// api/order.js import request from /utils/request export function getOrderList(params) { return request({ url: /order/list, method: get, params }) } export function completeOrder(orderId) { return request({ url: /order/complete/${orderId}, method: put }) }想维护的时候就只改这个文件别的地方都不用动。4. 核心功能模块拆解与实现重点这是全文最核心的部分。代码写得好不好看是一回事功能模块想得清不清楚是另一回事。模块想清楚了代码只是时间问题。4.1 用户认证与权限控制最具含金量汽车租赁系统有三类用户管理员、门店员工、普通租客。不同角色能访问的接口和页面必须区分开。这个系统里我建议用JWT RBAC的组合方案。JWTJSON Web Token解决的是“你是谁”的问题。登录成功后后端生成一个 token 令牌前端后续请求都带上这个 token后端验证通过后才返回数据。它和传统 Session 方案相比天然适合前后端分离服务端不需要保存会话状态。大概流程是用户登录后端校验用户名密码验证通过后生成一个 token 返回。前端在 utils 封装好的 axios 拦截器里从 localStorage 取出 token放进请求头Authorization: Bearer token。后端定义一个拦截器HandlerInterceptor对所有请求除了登录等白名单进行 token 解析验证。解析出的用户信息放进 ThreadLocal后续业务代码里可以直接获取当前登录用户。token 的生成可以用 jjwt 库核心代码大概是public String createToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }注意我给这个 token 设置了两小时过期时间时间太短用户用一会儿就掉线很恼火太长又不安全。两小时算是一个实操经验值你可以根据需求调整。RBAC基于角色的访问控制解决的是“你能干什么”的问题。最简单的方案是数据库里建三张表user、role、user_role用户关联角色每个角色对应一组菜单权限。但考虑到这是毕设你可以简化成在用户表里直接加一个role字段用字符串区分ADMIN、EMPLOYEE、CUSTOMER。前端拿到用户角色后动态生成菜单后端在拦截器里对敏感接口比如管理员才能调用的接口做角色校验。千万不要只做前端控制因为接口如果被直接调用前端那层控制就是形同虚设。前后端都要做权限控制这是我一直强调的安全底线。4.2 车辆信息管理车辆表是整个系统的“货物基础”没有车这个系统一切功能都没有载体。车辆实体起码要包含这些字段车牌号、车辆品牌建议用独立字典表方便做筛选、车型轿车/SUV/商务车等、颜色、座位数、变速箱类型自动挡/手动挡、日租金、押金、车辆照片一个 URL 字段配合文件上传功能、当前状态可用/已租/维修/保养、公里数、年检到期日。车辆照片的上传这里有两个选择。一是把图片上传到服务器本地目录数据库只保存相对路径二是使用 MinIO 搭建一个简易对象存储服务把图片传上去返回一个 URL。推荐后者因为你在简历里多写一句“使用了 MinIO 进行文件存储”含金量立刻就不一样了。MinIO 的搭建不需要运维知识下载下来配置一下就能跑。4.3 订单流程管理这个系统的心脏订单是业务的核心它具有明确的“状态机”。状态机这个词听起来高级实际意思就是“一个订单在不同时刻只能处于某个确定状态而且状态之间的流转是有方向的、需要条件的”。我建议你设计四个核心状态待取车PENDING客户提交订单管理员审核通过后生成租车单。此时车被锁定别人不能租。租用中RENTING客户到门店取车管理员确认发车。订单关联车辆状态改为“已租”。待归还DUE到了预计还车时间还没还。系统应该自动提醒这就是一个定时任务可以发挥的地方。已完成COMPLETED客户归还车辆管理员验车确认结算租金订单关闭。订单状态变化的每一次操作都要记录到order_log表谁在什么时间把订单从什么状态改成了什么状态。这个日志表在毕设阶段可能看起来“无关紧要”但答辩的时候一提到“操作留痕、审计追踪”这就是亮点。到时候你就能有理有据地告诉评委这个系统不是玩具它考虑了真实业务中的纠纷场景。4.4 租金计算与结算规则租金计算是纯业务问题但也是最能体现你是否“入行”的地方。真实租车公司有一个公式不能瞎设计总租金 日租单价 × 租用天数 超时费用 其他费用如保险选装 - 优惠折扣 押金 单独计算还车后无问题则退还“租用天数”不是简单算两个日期之间差多少天因为还车时间是按小时算的。真实场景中会用到“租车时长超出整日但不足一天”的计费规则。你可以在后端设计一个计算工具类专门处理时间差和费用计算。这个工具类的入参是下单时间、取车时间、计划还车时间、实际还车时间、日租金单价。返回值是详细的费用明细。这里有一个很重要的点不要让前端计算租金。前端只负责展示一切金额计算必须由后端完成。原因很简单前端传过来的金额是可以被篡改的。如果你让前端传一个“总价 500 元”过来懂点 HTTP 简单到不行的人完全可以把请求拦截下来改成 5 元。正确的姿势是前端只传“租用车辆的 ID、开始时间、结束时间”后端根据库里的单价算出总价。4.5 统计报表与数据可视化报表是答辩环节最能展示成果的模块。建议你用 ECharts 做三个图表近 6 个月营收趋势折线图按月汇总完成的订单总金额。车型出租占比饼图统计不同车型的出租次数占比。车辆出租排行柱状图出租次数最多的 Top10 车辆。后端的统计 SQL 会复杂一点但这就是 SQL 的学习机会。给你一个例子统计每月营收SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(total_amount) AS revenue FROM order WHERE status COMPLETED GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month编译好以后后端组装成图表数据结构返回前端前端直接用 ECharts 渲染。这一个模块可以充分展示你的 SQL 聚合能力和数据可视化能力。5. 数据库设计表结构搭得好业务就跑不偏5.1 数据表整体规划我建议整体设计 9 张核心表。附一张完整清单你可以对照着设计表名作用关键字段user用户表id、username、password加密、real_name、phone、role、statuscar车辆表id、plate_number、brand、model、car_type、color、seats、daily_rate、deposit、photo、statuscar_brand品牌字典表id、brand_name车站品牌尽量用字典维护不要写死在代码里order订单表id、order_no、user_id、car_id、start_date、end_date、actual_return_date、daily_rate、total_amount、deposit、statusorder_log订单日志表id、order_id、operator_id、action、from_status、to_status、remark、create_timeinsurance保险选项表id、name、price租车时可以加选保险这个表让项目更真实customer_feedback客户反馈表id、order_id、user_id、rating、content、create_timenotice公告表id、title、content、create_user、create_time不要过度建表也不要欠设计。比如车辆品牌有的学生直接把它写成车表里的一个字符串字段看起来没问题但当你需要统计“哪个品牌最受欢迎”的时候你会发现自己没法按品牌聚合。这种因为设计偷懒带来的后端烦恼我真的见得太多了。5.2 核心表设计时要注意的几个坑密码安全这是老生常谈了。密码绝对不能明文存。用哈希加盐或者直接用 Spring Security 自带的好消息BCryptPasswordEncoder。Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }存密码的时候是把密码先通过 encoder 加密再入库存登录校验时也是把用户传的明文密码跟数据库的 hash 比对而不是直接查出来对比。对这门课程的作业来说“登录功能”你做出来了不代表严谨密码加密这种细节评委很容易问到你答上来就是加分项。金额字段类型千万别用 float 或者 double。浮点数在计算机里通常不是精确的0.1 0.2 的结果可能是不等于 0.3 的。存储金额一律用DECIMAL(10, 2)。你在 Java 里用BigDecimal进行操作避免精度陷阱。这个知识点太常问了。时间字段建议业务时间用DATETIME如果要考虑时区问题可以统一使用TIMESTAMP。但更实用的建议是后端默认使用北京时间存入前端展示时格式化输出这样体验最统一。逻辑删除不要物理删除数据。用户注册了账号想注销车辆信息录错了要删掉正常的做法不是 DELETE而是在表里加一个deleted字段0 表示存在1 表示删除查询时统一在 SQL 加WHERE deleted 0。这个习惯在真实企业项目里几乎是标配论文里能写答辩里能讲。6. 实操把项目从零跑起来6.1 环境准备不管你是从开源平台下载的源码还是自己逐步开发你需要准备的环境基本是这几样JDK 8 或 11SpringBoot 2.x推荐 JDK 8 就够Maven 3.6Node.js 14 和 npmVue 2 项目建议 node 16我用 node 16 跑 Element UI 从未出问题Vue 3 Vite 建议 node 18MySQL 8.0IDEA或者 Eclipse但 IDEA 免费社区版真的够用Navicat 或 DataGrip 作为数据库管理工具MySQL 的安装有几个坑值得提醒Windows 上装 MySQL 8.0 时初始化的默认字符集是 utf8mb4这个没问题但是安装完成以后要记得确认 root 用户的鉴权方式。如果你用很老的客户端连接会出现 “Client does not support authentication protocol requested by server” 这种报错。解决方案是修改 root 用户的加密方式ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;如果你是拿现成的源码这一步卡住的人非常多报错群里十个有八个是这个原因。6.2 后端启动步骤用 IDEA 打开后端目录等待 Maven 自动下载依赖。这一步建议耐心等待如果下载慢可以配置阿里的镜像源。本地创建数据库car_rental字符集选择utf8mb4。找到源码目录里的car_rental.sql文件用 Navicat 打开执行导入表结构和初始数据。修改application.yml里的数据库连接信息主要是url、username、password。运行启动类看到 “Started CarRentalApplication” 日志说明 SpringBoot 启动成功。application.yml里的配置大概长这样server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/car_rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这个配置很有用它能让数据库的字段比如plate_number自动映射成实体的驼峰属性plateNumber避免写一堆 resultMap 别名。6.3 前端启动步骤用 WebStorm 或者 VS Code 打开前端目录。终端执行npm install安装依赖。如果项目里的package.json使用的依赖版本比较老npm 安装过程可能会有警告先忽略。执行npm run serve启动开发服务器。默认地址是http://localhost:8080打开以后登录页面连不上后端大概率是跨域问题。解决方式有两种后端配置 Cors 过滤器处理 CORS 跨域或者前端配置开发代理。前端vue.config.js代理方案更方便module.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样前端发的/api/login替你把请求转发给后端的8081端口。记住target是你后端实际端口且不要填错这个错误根本查不出来因为浏览器控制台报的是 “404”。7. 常见问题与排查技巧做这个项目会遇到的问题我大概归类成四类启动类、数据库连接类、接口联调类、前端报错类。我直接给你速查表。7.1 启动类问题SpringBoot 启动报错Unknown character set encoding: utf8检查application.yml是否把characterEncoding拼错了报错Consider defining a bean of type xxxMapper检查启动类是否漏了MapperScan或者确认 Mapper 接口有没有加Mapper注解。如果启动时端口被占用Tomcat 默认 8080 被占用两个解决方案关掉占用端口的进程推荐先看任务管理器或者在配置文件里把server.port改成别的端口。前端和后端的端口别搞成同一个否则你会看见极具迷惑性的错误。7.2 数据库连接类问题最常见的我刚刚讲过MySQL 8 以上版本需要明确使用com.mysql.cj.jdbc.Driver老版本的驱动类名com.mysql.jdbc.Driver在 8.0 里已经不推荐使用了。另一个高频报错是Public Key Retrieval is not allowed。这个问题发生在 MySQL 8.0 使用 SSL 安全连接时最简单粗暴的解决方式是在数据库连接 URL 后面加两个参数?useSSLfalseallowPublicKeyRetrievaltrue这个报错出现的原因我说一下MySQL 8.0 默认使用 caching_sha2_password 认证客户端连接时要么走 SSL要么用 RSA 公钥加密传输密码。JDBC 默认不允许获取公钥所以直接把allowPublicKeyRetrievaltrue加上就解决。顺带把 SSL 关掉本地开发完全不需要。7.3 接口联调类问题前端请求后端的接口报 401说明 JWT 校验失败。按这几步检查是否在登录之后正确保存了 token在localStorage.setItem(token, res.data.token)。是否在 axios 请求拦截器里带了Authorization头后端的拦截器是否放行了“免登录”接口比如登录接口本身前端请求接口报 500优先去 IDEA 的控制台看完整异常堆栈。大多数 500 都是后端 NPE空指针或 SQL 写错了控制台会直接告诉你哪一行代码出的问题。新手最容易犯的错是只看浏览器里的报错不去看后端的日志绕了很远的路。7.4 前端打包与部署问题毕设经常会有一个要求“把项目部署出来给老师现场演示”。如果老师要求能在服务器上跑你需要把 Vue 打包npm run build成静态文件然后放到后端里托管。这里有个操作诀窍把打包出的dist目录里的文件复制到 SpringBoot 的src/main/resources/static目录下。这样你在启动后端的时候访问http://localhost:8081就能打开前端页面前后端一体运行给老师演示的时候非常方便。但这个方案有一个坑前端打包时设置的 API 地址必须是相对路径比如/api而不能是http://localhost:8081否则你打包复制过去以后前端页面还是会去请求 8080已不存在的地址。8. 学习路线怎么把这个项目吃透而不是背会这一步本来想写代码层面的指导但我想跟你们分享一些更实际的学习思路。很多学弟学妹拿到源码第一件事是想改系统名称、改作者然后准备直接拿去交。我不建议这样。数据库可以抄代码可以抄但是答辩那十五分钟你要靠自己对项目的理解活下来。老师极其容易问的一个问题就是“你这个订单模块是怎么设计的能不能讲讲”你支支吾吾那你作为第一作者的含金量就打了折扣。我建议你拿到一套源码以后先跑通再读懂最后自己重写一遍核心模块。具体你可以这样做先把项目跑起来体验一遍功能把一个完整的业务流程走通注册用户 - 查看车辆 - 下单 - 管理员审核 - 还车结算。打开数据库对着表结构把每张表之间的关系画出来搞清楚“用户怎么关联到订单的”、“订单怎么关联到车的”。从前端入手追踪一次完整的请求链路用户在页面点击了按钮请求发到了哪个 API 方法Vue Router 把用户带到了哪个页面axios 把请求发到了哪个后端 URLController 是哪个方法Service 里调了什么逻辑Mapper 查了哪些表最后怎么把数据传给前端渲染。把这个链路走一遍你基本就理解了这个项目的 80%。最后找一张简单的表比如车辆表自己从数据库设计开始到后端接口到前端页面完整地实现一遍。做过一遍的人才真正有底。一点个人体会我做这个项目的时候被 JWT 拦截器折腾了两天后来发现只是 return 了 true 但是忘了在 interceptor 里放行前置路由。那也是我第一次意识到为什么认识代码和代码报错时“两眼一抹黑”之间的差距那么大。做毕设真正的意义不是学会某个框架而是第一次独立地面对一个不完整的、模糊的、带着无数报错的需求然后把它拆开慢慢捋顺最后做出来一个自己能完整讲清楚的东西。工具和框架只是手段业务模型和排查思维才是你在这个项目里真正能学到的东西。祝你把项目跑通答辩顺利。
返回列表