ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot汽车租赁管理系统:从设计到实战的完整指南

SpringBoot汽车租赁管理系统:从设计到实战的完整指南

做毕业设计选题目,说是技术活,其实更大程度上是个体力活。我这两年陆陆续续帮学弟学妹看过不少题目,发现“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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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_time

status取0和1,是因为待取车和用车中的订单都占用了车辆的未来或当前时段。只要这个查询结果大于0,就直接拒绝下单。这个SQL写对,90%的“超租”问题就不会发生。做演示时,这也是一个效果很好的场景:用两个账号同时下同一辆车的重叠时段,系统必须把第二个请求拦下来。

3. 核心功能实现:从搭建工程到订单状态流转的完整落地

3.1 工程骨架与关键依赖

工程结构用最常规的分层:

com.example.rental ├── controller // 接口层 ├── service // 业务层,事务写在这 ├── mapper // MyBatis-Plus Mapper ├── entity // 实体类 ├── config // 拦截器、跨域、分页插件配置 ├── common // 统一返回、异常处理 └── RentalApplication.java

pom.xml里核心依赖就这几组:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency>

application.yml里有两个容易出错的点:数据源url要带serverTimezone=Asia/Shanghai,否则时间会差8小时;MyBatis-Plus要开启下划线转驼峰,字段映射才对得上:

spring: datasource: url: jdbc:mysql://localhost:3306/rental?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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. 登录用户端,进入车辆列表,展示筛选和车辆状态。
  2. 选一辆车下单,故意选一个会产生冲突的时间段,展示系统弹出“该时段已被预订”的提示。这一手能证明你不是摆设,校验是真的。
  3. 正常选择一个不冲突的时间下单,在“我的订单”里看到待取车状态。
  4. 点取车,订单变为用车中,再回到车辆列表,这辆车状态已经变成已租。
  5. 点还车,展示费用明细,比如用车1小时20分钟,怎么从分钟数算成金额的。
  6. 切到管理端,展示订单追踪、车辆管理、统计图表。

演示前一定要准备好数据和账号,现场注册、现场造数据是演示事故的高发区。

5.2 高频提问和应答思路

答辩老师问的问题其实高度集中,提前准备好应答思路就不慌:

提问方向应答要点
为什么用JWT不用Session无状态、跨端友好、集群部署不需要共享存储;补充Redis黑名单解决Token失效问题
怎么防止同一辆车同一时间被重复下单InnoDB行锁FOR UPDATE加时间重叠校验,事务保证一致性,按代码顺序讲
车辆状态和订单状态会不会不一致所有状态变更收敛在事务内,取消和超时都走同一套回收逻辑
系统怎么扩展成真正的共享汽车平台增加定位、车辆开关锁、分时计价接口,核心订单模型和状态机不用动

答辩最忌讳的是只会念代码。你把每个设计决策背后的“为什么”讲清楚,哪怕实现上有一些简化,老师也不会深挖。尤其并发控制和时间重叠这两个点,能主动画图讲明白,这一部分基本就稳了。

最后聊一点我自己的感受。这个题目我前前后后帮着落地过好几轮,最大的心得不是某段代码多精妙,而是先把状态流转画对再动手。很多同学做管理系统,花最多时间在页面上,结果业务逻辑一问就露馅。汽车租赁系统真正的门槛恰恰在状态机、并发和结算这三件事上,你把这三个点想透了,代码只是把它们翻译出来而已。如果你时间来得及,建议在答辩前把超时关单和时间冲突的演示各练一遍,这两个场景能讲清楚,这门设计基本就站得住脚了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进