做这套汽车租赁管理系统的时候我其实是拿它当“新手到进阶”的过渡项目来打磨的。团队里几个想走 Java 后端路线的年轻人需要的是能完整走通“数据库设计→后端接口→前端页面→部署上线”全流程的项目但又不想用那些烂大街的电商秒杀系统最后定下来做汽车租赁。原因很简单租赁业务天然包含车辆、客户、订单、押金、超时费、违章、保养这些真实场景足够把 CRUD 之外的业务逻辑讲清楚又不会复杂到让新手直接放弃。SpringBoot、Vue3、MyBatis、MySQL 这套组合在中小型管理系统里几乎是“黄金搭档”。SpringBoot 负责快速搭服务端骨架MyBatis 给了你对 SQL 控制的自由度Vue3 做前后端分离的交互层MySQL 应付这种体量的数据完全够用。如果你正在寻找 Java 后端实际项目经验或者需要一个能直接二次开发的租赁管理系统这篇文章值得你花十分钟看完。我会把技术选型的理由、数据库设计、核心模块实现、前后端联调踩坑、部署经验都摊开讲代码不会贴完整工程但关键片段和思路都会给到。1. 项目定位与整体设计思路1.1 这套系统到底解决什么问题汽车租赁管理系统表面看是一堆增删改查实际上业务场景很立体。租车公司、做校园租车的团队、想搞长短租结合的运营方都需要管理几件事车辆从入库、保养、出险到报废的全生命周期状态客户从注册、实名认证到押金记录租赁订单从预订、取车、还车到结算以及衍生出来的超时费、违章处理、保险记录。我把它拆成三个核心域车辆域车辆信息、品牌型号、车牌号、车辆状态空闲/已租/维修/保养/下线、日租金、押金、行驶里程。客户域客户信息、驾驶证信息、联系方式、押金账户、租车历史、黑名单状态。订单域租车订单、还车记录、费用结算、违章记录、续租记录。这三个域交叉的地方就是核心业务规则。比如下订单时要校验车辆状态和客户是否有未结清的违约金还车时要根据超时小时数、油量差、里程差自动计算费用客户在订单未完成期间不允许再次租车。这些逻辑如果只靠前端判断用户随手刷新或者直接调接口就能绕过所以必须由后端保证这也是我强调“业务逻辑下沉”的原因。1.2 为什么选 SpringBootVue3MyBatis而不是其他组合SpringBoot 的好处不用多说约定优于配置、内嵌 Tomcat、一个 main 方法就能起服务。相比 SSM 时代那一堆 XML 配置SpringBoot 的核心价值在于让团队把时间花在业务代码上而不是配置上。这里补充一个热词里大家很关心的问题SpringBoot 版本怎么选如果你刚接触建议直接选最新的稳定 3.x 系列选型但要注意 JDK 必须 17 以上、MySQL 驱动用 8.0.33 以上版本。如果团队环境还是 JDK 8、或者依赖老项目那就回退到 2.7.x 系列千万别出现“版本拉太高导致启动报错”的情况。MyBatis 选它的理由更直接租赁业务的查询非常看重 SQL 的灵活控制。车辆列表可能需要按品牌、价格区间、座位数、变速箱类型做多条件组合筛选订单统计要按天、按周、按月 group by还车结算可能涉及多张表关联。这种场景下JPA 自动生成的 SQL 会让你调试到怀疑人生而 MyBatis 的 mapper 文件里写什么就是什么性能可预期也方便 DBA 评审。另外 MyBatis 二级缓存这个话题是 Java 面试里常被问的我在 2.3 节会单独说怎么用才不踩坑。Vue3 的优势在 Composition API 和响应式系统的重构。Vue2 里一个复杂页面 data 和 methods 混在一起几百行代码非常散Vue3 的 setup ref reactive 可以把逻辑按业务拆分代码可读性明显上升。再加上 Vite 的开发体验热更新速度比 Webpack 时代的 Vue2 项目舒服太多。如果你之前只会 Vue2 选项式 API也不用慌Vue3 完全兼容原有写法你可以先平滑迁移再逐步使用组合式 API。MySQL 没什么好纠结的管理系统里 99% 的场景它都能扛住。关键在建表规范、索引设计、事务隔离级别这些才是真正决定系统稳不稳的地方。1.3 前后端分离的架构与目录组织前后端分离不只是“前端一个项目、后端一个项目”还要把接口约定、鉴权方式、跨域方案、联调环境这些配套问题想清楚。后端我习惯的包结构是这样的com.example.rental ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # MyBatis 数据访问层 ├── entity # 数据库实体 ├── dto # 接口入参对象 ├── vo # 前端展示对象 ├── config # 全局配置跨域、拦截器、MyBatis ├── common # 统一返回结果、异常处理 └── utils # JWT、日期、金额等工具有人问过为什么还要拆 dto 和 vo直接用 entity 传给前端不行吗行是行但有两个问题。第一entity 里的密码哈希、创建人 ID 等字段没必要暴露给前端第二前端需要的展示字段往往要从多张表聚合比如订单列表要同时显示车型名称、客户姓名、车牌号这根本不在同一张表里。所以接口层永远只返回 vo 或 dto形成规矩之后前后端联调会顺畅很多。前端我用 Vue3 Vite Pinia Vue Router Axios Element Plus。Element Plus 的表格、表单、弹窗组件对管理后台太友好了哪怕 UI 能力一般也能搭出像样的后台界面。2. 数据库设计与核心表结构2.1 MySQL 8.0 的安装与基础配置MySQL 8.0 是现在的主流选择字符集默认 utf8mb4支持窗口函数和 CTE性能也比 5.7 有提升。安装方式两种都提一下Docker 安装最省心docker run -d --name mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORD你的密码 mysql:8.0。注意数据目录一定要挂载出来否则容器一删数据全没。RPM 或 tar 包安装适合内网服务器安装后记得执行systemctl start mysqld初始密码在日志里/var/log/mysqld.log用grep temporary password找。几个关键配置点连接串必须带三样东西useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。时区不写8.0 版本下会直接报“The server time zone value”错误。建库时直接指定字符集CREATE DATABASE car_rental DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;千万别用默认 latin1否则中文乱码排查起来极其痛苦。MySQL 8.0 默认密码插件是 caching_sha2_password如果客户端驱动太老会报 Unable to load authentication plugin。要么升级驱动到 8.0.33 以上要么创建用户时指定 mysql_native_password。2.2 核心表设计我设计的核心表有这么几张。车辆表 carCREATE TABLE car ( id BIGINT PRIMARY KEY AUTO_INCREMENT, car_no VARCHAR(20) NOT NULL COMMENT 车牌号, brand VARCHAR(50) NOT NULL COMMENT 品牌, model VARCHAR(50) COMMENT 车型, color VARCHAR(20), seats TINYINT DEFAULT 5 COMMENT 座位数, gearbox VARCHAR(10) COMMENT 手动/自动, daily_price DECIMAL(10,2) NOT NULL COMMENT 日租金, deposit DECIMAL(10,2) COMMENT 押金, status TINYINT DEFAULT 0 COMMENT 0空闲 1已租 2维修 3保养 4下线, mileage DECIMAL(10,1) DEFAULT 0 COMMENT 总里程(km), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_car_no (car_no) ) ENGINEInnoDB;几个设计细节。金额一律用 DECIMAL别用 float/double否则算租金、押金、违约金时出现 0.30000000000000004 这种数字客户前台都看懵了。状态字段用 TINYINT 而不是字符串查询性能好、存储省代码里用枚举一一映射。车牌号加唯一索引因为一辆车只可能有一个“身份”。客户表 customer 包括 id、name、id_card身份证号、driver_no驾驶证号、phone、status0正常 1黑名单、create_time。身份证号加唯一索引手机号也加索引登录和查询都靠它。订单表 rental_order 是整个系统的核心CREATE TABLE rental_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, customer_id BIGINT NOT NULL, car_id BIGINT NOT NULL, rental_start DATETIME NOT NULL COMMENT 实际取车时间, rental_end DATETIME NOT NULL COMMENT 预计还车时间, actual_return_time DATETIME COMMENT 实际还车时间, daily_rent DECIMAL(10,2) NOT NULL COMMENT 租期内日租金, total_amount DECIMAL(10,2) NOT NULL COMMENT 应收总额, actual_amount DECIMAL(10,2) COMMENT 实收金额, deposit DECIMAL(10,2) COMMENT 押金, status TINYINT DEFAULT 0 COMMENT 0待取车 1租赁中 2已还车 3已结算 4已取消 5逾期未还, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_customer (customer_id), KEY idx_car (car_id), KEY idx_status (status) ) ENGINEInnoDB;这里有一个非常值得展开的业务细节订单表里存了 daily_rent 这个“冗余字段”。为什么因为车辆的日租金今天可能调价、明天可能做活动客户还车时如果你去读“当前”租金来结算一定吵架。正确做法是下单时把租金快照进订单这就是经典的价格快照思想。这个细节我后来在 Java 面试题里也见过面试官问“价格会变动的商品订单表怎么设计”答案就是快照 冗余字段。另外我单独建了一张业务参数表 config放超时费率、保险费率、单日里程限制等全局配置避免写死在代码里。2.3 MyBatis 映射、多表联查与缓存使用表建好之后MyBatis 的 mapper 就是对 SQL 的翻译层。我强烈建议复杂的查询不要用注解Select堆在接口上而是写 XML mapper。理由很简单多表联查的 SQL 动辄十几行写在 XML 里可以做动态 SQL 拼接格式清晰维护的人能直接复制到 Navicat 里跑。车辆列表的多条件筛选是典型场景select idlistCars resultTypecom.example.rental.vo.CarVO SELECT id, car_no, brand, model, color, seats, gearbox, daily_price, deposit, status, CASE status WHEN 0 THEN 空闲 WHEN 1 THEN 已租 WHEN 2 THEN 维修 WHEN 3 THEN 保养 ELSE 下线 END AS statusName FROM car where if testbrand ! null and brand ! AND brand LIKE CONCAT(%, #{brand}, %) /if if testminPrice ! null AND daily_price gt; #{minPrice} /if if testmaxPrice ! null AND daily_price lt; #{maxPrice} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /select注意 XML 里小于号必须转义成lt;这是一个新手必踩的坑直接写会解析失败控制台飘一片 SAXParseException。订单列表联查客户和车辆信息也是两条 LEFT JOIN 搞定但要注意字段重名问题两张表都有 create_time 或 status 时SELECT 里必须用别名区分否则 MyBatis 映射时后面字段值会把前面覆盖掉页面数据对不上排查起来非常隐蔽。顺便聊聊 MyBatis 缓存。MyBatis 一级缓存是 SqlSession 级别的默认开启一次事务里相同查询直接命中缓存二级缓存是 namespace 级别的在 mapper XML 里加cache/就能开启。但我在这个项目里只给“车辆品牌字典”这种极少变更的查询开了二级缓存订单、客户、车辆状态这类数据一律不开。为什么因为缓存刷新时机很难把控一旦订单状态变了而缓存没失效客户看到的还是“待取车”那就是事故。面试被问到 MyBatis 缓存时能说出“二级缓存适合读多写少、变更频率极低的数据业务核心数据不建议开”这个结论比背概念强一百倍。3. 后端核心业务模块实现3.1 登录与权限JWT 拦截器管理系统的登录我推荐 JWT 而不是 session。前后端分离之后前端跑 5173、后端跑 8080session 天然有跨域问题cookie 的 SameSite 策略也会把 session 卡死。JWT 的 token 由前端放在请求头 Authorization 里传递后端拦截器解析校验天然适配分离架构。登录逻辑的核心片段Service public class AuthService { public LoginResponse login(LoginRequest req) { Customer customer customerMapper.findByPhone(req.getPhone()); if (customer null) { throw new BusinessException(手机号未注册); } if (!BCrypt.checkpw(req.getPassword(), customer.getPassword())) { throw new BusinessException(密码错误); } if (customer.getStatus() 1) { throw new BusinessException(账户已被限制请联系客服); } String token JwtUtil.createToken(customer.getId(), customer.getName()); return new LoginResponse(token, customer); } }密码千万别明文存储用 BCrypt 加密。Spring Security 里自带 BCryptPasswordEncoder不想引入整个 Spring Security也可以用 jBCrypt 库。为什么不用 MD5因为 MD5 无盐、同密码同哈希撞库太容易BCrypt 自带盐值每次加密结果都不同这才是存密码该用的算法。JWT 时效建议设置 2 小时前端拿到 token 存 localStorageAxios 拦截器统一带axios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config })后端拦截器校验 token务必放行登录接口、静态资源、以及前端跨域预检 OPTIONS 请求否则联调时前端预检直接 401半天定位不到。3.2 车辆管理状态流转与并发控制车辆管理里最容易出 bug 的是“同一辆车被两个人同时下单”。业务规则是客户选车、提交订单、到店取车时车辆从空闲变为已租。但如果两个请求同时读到同一辆车状态是空闲都去做更新就会发生超卖。解决思路分两层。第一层是数据库层面更新时加状态条件利用行锁。int rows carMapper.updateStatusWithCondition(carId, oldStatus, newStatus); if (rows 0) { throw new BusinessException(车辆状态已变化请刷新重试); }对应 SQL 是UPDATE car SET status #{newStatus} WHERE id #{carId} AND status #{oldStatus}。影响行数为 0说明其他请求先改了状态这个请求直接失败不会产生脏数据。第二层是程序层面对同一车辆的订单操作加锁。对于单体管理系统数据库条件更新已经足够完全没必要上 Redisson 分布式锁徒增复杂度。这个场景也是面试常考的点乐观锁版本号或条件更新和悲观锁SELECT FOR UPDATE的区别以及各自适用场景。车辆保养和维修建议单独建一张 car_maintenance 表记录维修开始时间、结束时间、费用、原因、维修厂。车辆履历完整了后续二手车折旧评估、保险理赔都有依据。3.3 租赁订单的状态机设计与还车结算订单模块是整个系统的状态中枢。我设定了这些状态0 待取车、1 租赁中、2 已还车、3 已结算、4 已取消、5 逾期未还。状态机的好处是让状态迁移可控。只有“待取车”状态才能取消订单只有“租赁中”才能登记还车只有“已还车”才能做结算。service 层写一个私有方法校验迁移合法性private void checkTransition(RentalOrder order, int expectStatus, int targetStatus) { if (order.getStatus() ! expectStatus) { throw new BusinessException(订单当前状态不允许该操作请刷新后重试); } order.setStatus(targetStatus); }还车结算的逻辑最值得单独讲。客户还车时系统要按实际情况算费用基础费用 日租金 × 租用天数租用天数按实际还车时间减实际取车时间不足一天按小时折算。超时费实际还车时间晚于预计还车时间超时部分按超时费率计算我配置为日租金的 10% 每小时。里程费取车时记录里程还车时里程超出约定额度按单价补收。油费差额取车时满油还车时少油按市价补收。违章押金暂不结算15 天后查询无违章再退还。这些费用计算如果全堆在 if/else 里代码会越来越乱。我用策略模式把费用项拆成独立计算器后续加“节假日上浮”“新客立减”只需要新增一个策略实现类不用动主流程。public interface SettleStrategy { BigDecimal calculate(RentalOrder order, ReturnCarDTO dto); } Component public class BaseRentStrategy implements SettleStrategy { Override public BigDecimal calculate(RentalOrder order, ReturnCarDTO dto) { long hours Duration.between(order.getRentalStart(), dto.getReturnTime()).toHours(); BigDecimal days BigDecimal.valueOf(Math.max(1, (hours 23) / 24)); return order.getDailyRent().multiply(days); } }如果你不想用策略模式这种略重的设计用 Map 存接口实现类也能达到类似效果原则就一个别把所有计算挤在一个方法里否则改一次费用规则就要动整个流程。4. 前端 Vue3 开发实战4.1 用 Vite 初始化项目创建项目用npm create vitelatest选 vue 模板。项目结构大致如下src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由 ├── stores/ # Pinia 状态管理 ├── views/ # 页面 ├── App.vue └── main.jsVite 默认端口是 5173后端接口是 8080本地开发必须配代理。在 vite.config.js 里这样写server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求 /api/login 就自动转发到后端绕过跨域。生产环境则用 Nginx 反向代理统一入口。如果你要用 SassVite 支持也很好npm install sass之后直接在 style 标签里写 langscss 就行。4.2 用 Pinia 管理登录状态Vue3 官方推荐用 Pinia 替代 Vuex学习成本还低。登录成功后把用户信息存到 storeexport const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: {} }), actions: { async login(phone, password) { const res await loginApi({ phone, password }) this.token res.token this.userInfo res.customer localStorage.setItem(token, res.token) }, logout() { this.token this.userInfo {} localStorage.removeItem(token) } } })路由守卫做登录校验管理后台必须做未登录拦截否则用户直接敲 URL 就能进系统那权限体系形同虚设router.beforeEach((to) { const store useUserStore() if (to.path ! /login !store.token) { return /login } })4.3 页面列表与表单的组件化思路管理后台的页面长得很像表格 筛选区 新增/编辑弹窗。千万别每个页面试着复制粘贴我抽了一个通用表格配置代码量直接减半。Element Plus 的 el-table 支持传 columns 动态渲染const columns [ { prop: orderNo, label: 订单号, width: 200 }, { prop: customerName, label: 客户姓名 }, { prop: carNo, label: 车牌号 }, { prop: rentalStart, label: 取车时间 }, { prop: status, label: 状态, slot: statusSlot }, { prop: totalAmount, label: 应收金额, formatter: (row) ¥ Number(row.totalAmount).toFixed(2) } ]表格里的状态字段最好用 el-tag 标签展示不同状态不同颜色一眼扫过去就知道哪些订单逾期、哪些在租赁中体验完全不一样。表单校验也别偷懒。Vue3 Element Plus 的 form validation 是声明式的身份证号、手机号既要有必填校验又要有格式校验。建议前端先拦一道后端再做最终校验前端校验的意义是快速反馈后端校验的意义是数据安全两者不是二选一的关系。4.4 订单创建页面的交互细节订单创建可能是前端最复杂的页面要点有这几个选择客户搜索下拉联查客户信息。选择车辆列表展示车辆状态已租、维修中的车辆要禁用。选择租期日期时间范围选择器。实时估算费用监听租期变化调用计算接口或前端近似计算展示预估费用。提交校验通过后调用订单创建接口。这里有一个前端小坑el-date-picker 返回的是 Date 对象后端要的是字符串提交前必须格式化。我统一封装了一个 formatDateTime 工具函数防止“后端收到日期格式不对”这种低级 bug。另一个坑是跨天租期开始日期到结束日期可能跨越好几天前端估算费用时按天取整跟后端按小时折算的结果会有偏差。解决方法是估算只给参考值最终以后端结算为准界面上明确写“费用以实际还车结算为准”。5. 常见问题与排查实录5.1 MySQL 安装与连接报错新人最容易卡住的是连接串我汇总一下高频报错。报错信息原因解决方案Public Key Retrieval is not allowed连接串缺少参数加 allowPublicKeyRetrievaltrueCommunications link failure / Server time zone value时区未指定加 serverTimezoneAsia/ShanghaiAccess denied for user rootlocalhost用户权限问题确认密码远程连接单独授权SSL 连接错误客户端与服务端 SSL 协商失败加 useSSLfalse或更换驱动版本这些错误会轮流出现在 IDEA、Navicat、后端控制台别慌从头读完整报错信息90% 的问题都出在连接串。RPM 安装的 MySQL 还要留意初始密码藏在日志里忘了这一步连登录都进不去。5.2 MyBatis 经典报错BindingException: Invalid bound statement (not found)mapper 接口和 XML 没有对应上。检查 namespace 是否等于接口全限定名检查 SpringBoot 配置里的 mybatis.mapper-locations 是否扫到 XML。参数绑定问题接口方法传多个参数时必须加 Param 注解否则 XML 里只能用 param1、param2 这种丑陋的名字。查询结果全是 null大概率是驼峰映射没开。配置里加一行map-underscore-to-camel-case: true数据库下划线字段就自动映射到 Java 驼峰属性。没加的时候 SQL 没错、接口也没错就是属性填不上这种问题最坑。5.3 前端联调时的跨域与 401前端本地开发配了代理之后跨域基本解决。但还有一个经典问题后端 CORS 配置写了 allowedOrigins前端也配了 proxy结果登录接口正常带 token 的接口却 401。原因通常是后端的跨域过滤器把 OPTIONS 预检请求拦截了。SpringBoot 里这样配最省心Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }同时登录拦截器里放行 OPTIONSif (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }另外一个容易被忽略的点allowedOriginPatterns() 和 allowCredentials(true) 要搭配使用。Spring 5.3 之后对 allowedOrigins() 加 allowCredentials(true) 的组合处理得很严格直接用 allowedOriginPatterns 最省心。5.4 数据库连接池耗尽问题管理系统访问量不大但连接池被占满的情况也时有发生。主要原因通常是事务里调外部接口、慢 SQL 太多、连接没有归还。排查思路打开连接池监控看活跃连接数和等待时间。HikariCP 可以配hikari: { pool-name: RentalHikariPool }再通过 actuator 暴露指标。检查代码里是不是有在循环里调 mapper 的坏习惯比如 for 循环里逐个查客户信息改成批量 JOIN 或 IN 查询。所有慢查询用 EXPLAIN 看执行计划。rental_order 的 customer_id、car_id 都加索引order_no 加唯一索引别让全表扫描把连接池拖死。6. 打包部署与实操心得6.1 后端打包与 systemd 管理SpringBoot 用 Maven 打包记得指定打包插件版本JDK 版本不同可能导致插件失效。mvn clean package -DskipTests打出来的 jar 直接运行。生产环境我用 systemd 管理崩溃自动重启日志落盘[Unit] DescriptionCar Rental Server Afternetwork.target [Service] ExecStart/opt/rental/car-rental.jar Restartalways RestartSec5 Userroot [Install] WantedBymulti-user.target启动后systemctl daemon-reload再systemctl start car-rental以后服务器重启服务自动拉起比自己写 nohup 脚本靠谱。6.2 前端构建与 Nginx 部署前端执行npm run build产物在 dist 目录。Nginx 配置一个 server 块把 / 指向 dist把 /api 反向代理到后端 8080server { listen 80; server_name your-domain.com; root /opt/rental/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }最后一行 try_files 是重点。前端用的 history 路由直接刷新 /order/list 时 Nginx 找不到对应文件必须回退到 index.html由前端路由接管。忘了这行部署完一刷新就 404这是最频繁的部署翻车现场。6.3 掏心窝的经验最后分享几条我做这个项目最有价值的体会。第一状态管理一定要集中。不要在接口里随手改订单状态又顺手改车辆状态订单和车辆是跨表的强一致场景必须用Transactional保证要么全部成功要么全部回滚否则线上一定出现“订单是租赁中但车辆是空闲”的脏数据。事务在单体阶段做好后面的复杂系统才不至于雪崩。第二做管理系统别一上来就上微服务。SpringBoot 单体 MySQL 完全能支撑中小租赁公司的业务微服务带来的分布式事务和服务治理复杂度在这个体量下是纯负收益。先把单体做扎实把事务、索引、备份这些基本功练好比追逐架构潮流重要得多。第三接口统一返回格式和统一异常处理一定要在项目初期就定好。我用{code, msg, data}包装所有返回RestControllerAdvice统一捕获异常。如果一开始没统一前端每个请求单独处理错误提示代码会丑到你不想维护后面再改接口风格前后端都要伤筋动骨。第四数据库备份一天都不能断。我用 mysqldump 每天凌晨自动备份保留最近 7 天一行 cron 搞定0 2 * * * mysqldump -uroot -p你的密码 car_rental | gzip /backup/rental_$(date \%F).sql.gz备份这东西平时看不见价值一旦数据出问题它就是你的救命稻草。这套系统从我第一次搭好到现在已经迭代过三轮最大的感触是汽车租赁这种业务技术难点从来不在某个单独的功能上而在于不同模块之间状态的协作。把状态机设计好、把并发边界想清楚、把事务和索引做扎实系统自然就稳了。如果你也打算拿这套源码练手我的建议是别只盯着页面能不能跑通多试试两个人同时订同一辆车、还车超时该怎么结算、客户黑名单状态下能否下单这些边界情况才是真正让你进步的地方。