news 2026/9/10 18:14:17

SpringBoot汽车租赁系统毕设:业务梳理、数据库设计与答辩全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot汽车租赁系统毕设:业务梳理、数据库设计与答辩全解析

又到了毕设产出集中的节点,论坛和社群里隔三差五就有人问:汽车租赁系统的 SpringBoot 毕设源码,有没有现成的能跑起来直接交?我的回答一直是:能跑起来的源码到处都是,能扛住评阅老师追问的源码才是真的值钱。这几年帮人评审过的毕设项目里,做得扎实的往往不是功能最多的,而是业务逻辑最清楚的。你从网上拉下来的每个项目,最怕的不是有bug,而是根本讲不明白它为什么这么设计。

这篇文章,我不打算给你贴一套完整代码充字数,而是想跟你聊清楚“基于SpringBoot的汽车租赁系统”这类毕设从业务梳理、技术选型、数据库设计到核心代码实现的完整链路。你拿到源码之后,对照这篇文章拆一遍,能动的部分动一动,能讲的部分讲明白,比单纯改个包名就交上去要稳妥得多。

1. 先拆业务骨架:汽车租赁系统到底在解决什么问题

1.1 一条订单的完整生命周期

很多同学上手就写代码,写到订单表就卡住了,因为没想清楚汽车租赁的本质业务流程。说到底,这是一个“资源—预约—履约—结算”的闭环业务。

我习惯把一个完整的租车流程画成下面这条线,这样无论代码怎么组织,心里都有一个地图:

用户注册登录 → 管理员在后台发布车辆 → 用户浏览/搜索车辆 → 用户选定租期、提交租车订单 → 管理员审核订单 → 审核通过后用户按期取车 → 用户用车 → 用户还车 → 系统根据租期和超时情况计算费用 → 订单完成

这中间有一条容易被忽视的并行线:管理员对车辆状态的管控。车辆不是永远可租的,它在订单审核通过的那一刻起就应该变为“已租”,否则就会出现同一辆车被两个人同时下单的尴尬局面。这条车与订单之间的状态联动,是整个系统最核心的部分。

把流程理解成一条线之后,你会发现所谓的“设计与实现”,其实就是把这条线上每一站的输入、输出、异常情况搞清楚,再用代码表达出来。

1.2 角色与功能清单:哪些必须有,哪些只是凑数

汽车租赁系统通常是双角色的,管理员和普通用户。功能上我建议分两类:一类是“没有就说不过去”的核心功能,一类是“有则锦上添花”的辅助功能。核心功能必须在五天以内能做完且做稳,辅助功能看剩余时间再决定。

先看用户端的核心功能:

  • 注册与登录(含密码加密存储)
  • 车辆列表浏览与关键字/品牌筛选
  • 车辆详情页
  • 下单租车(选择取车时间、还车时间)
  • 我的订单列表(全部、待审核、租赁中、已完成等状态筛选)
  • 取消订单(只在待审核状态下允许)

用户端的辅助功能:个人资料修改、押金充值记录、通知公告查看、租车评价。这些有时间就做,没时间不做也能交差。

再看管理端核心功能:

  • 管理员登录
  • 车辆管理(新增车辆、编辑信息、下架、上架)
  • 车辆列表(含条件分页查询)
  • 订单审核(通过/拒绝,拒绝要填原因)
  • 订单管理(查看所有订单,标记还车并结算)
  • 用户管理(查看用户列表,禁用/启用用户)

管理端的辅助功能:数据统计(每日订单量、收入汇总)、图片上传、公告管理。数据统计这块如果第四五章的技术点吃透了,做起来并不难,但如果时间紧张,留着不做也不影响主线逻辑。

这套功能清单列出来之后,你会发现它和酒店管理、图书管理、设备管理系统高度相似。本质上都是“资源 + 预订 + 状态管理”,只是资源的字段不同而已。你把这个共性想通了,后面看代码的时候就不会被各种业务细节带跑。

2. 技术选型定生死:为什么我推荐SpringBoot 2.7.x这一套组合

2.1 版本怎么选,别追新

毕设项目的第一原则是求稳,第二原则是资料多。如果你是用搜到的问题去排错,资料多的版本能让你少掉一半头发。

我建议你选这一套组合,这是现阶段最稳的:

组件推荐版本原因
JDK1.8 或 11大多数学校和教材还在用,兼容性最好
SpringBoot2.7.x资料极其丰富,能覆盖你遇到的绝大多数报错
MyBatis-Plus3.5.x单表CRUD几乎不用写SQL,省时省力
MySQL5.7 或 8.0两者皆可,8.0记得用8.0的驱动和URL参数
Maven3.8+稳定即可
前端方案Thymeleaf / Bootstrap+jQuery / Vue按你的前端水平三选一,后面第四章细说

SpringBoot 3.x虽然已经是很成熟的版本了,但要求JDK17起步,很多配套的依赖、博客教程和异常解决方案还停留在2.x时代。你花一个下午在版本适配上面,不如把这时间留给业务代码。记住:毕设的核心是业务逻辑完整、设计思路清晰,而不是用了最新版本。

2.2 创建项目失败的头号大坑:start.spring.io 拉不动

每次听到有人说“IDEA创建SpringBoot项目超时”,我第一反应就是网络问题。Spring官方的start.spring.io在国内的访问速度很不稳定,尤其下午高峰时段,很容易卡在加载页面。

解决方案有三个,按推荐顺序排:

  1. 改用阿里云镜像:在 IDEA 的 HTTP Proxy 或 Server URL 里填https://start.aliyun.com。这个镜像和官方结构几乎一样,速度稳定。
  2. 手动建了一个普通Maven项目:如果在IDE里始终创建失败,干脆不要用脚手架。新建一个Maven空项目,在pom.xml里手动写入spring-boot-starter-parentspring-boot-starter-webmybatis-plus-boot-startermysql-connector-java等依赖,再写一个启动类。这招最笨,但绝对不会被卡住。
  3. 换网络环境:如果公司或学校网络有代理限制,创建超时也可能是代理设置的问题。在IDEA的Settings → Appearance & Behavior → System Settings → HTTP Proxy里改成Auto-detect,或者关掉代理重试。

依赖下载慢是另一个问题,在settings.xml里加阿里云镜像是最常规的操作。Maven的仓库地址配置好了,后面所有依赖都是一次性拉完的。

2.3 包结构怎么摆,决定你后面改代码的心情

包结构是个容易被忽视但后期影响巨大的事情。我见过不少源码,所有类堆在一个包下面,controller、service、mapper混在一起,光找文件就要半天。

我推荐一个比较干净的划分,也是我最常用的方案:

com.example.carrental ├── CarRentalApplication.java // 启动类 ├── common // 通用工具与类 │ ├── Result.java // 统一返回体 │ ├── ResultCode.java // 状态码枚举 │ └── BizException.java // 自定义业务异常 ├── config // 配置类 │ ├── MybatisPlusConfig.java // 分页插件配置 │ ├── WebMvcConfig.java // 拦截器/跨域/静态资源映射 │ └── PasswordConfig.java // BCrypt编码器Bean ├── controller // 接口层 │ ├── UserController.java │ ├── CarController.java │ └── OrderController.java ├── service // 业务层 │ ├── UserService.java │ └── impl │ └── UserServiceImpl.java ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体类 ├── dto // 接收前端参数的类 ├── vo // 返回给前端的视图对象 └── interceptor // 登录拦截器

这个结构的关键点在于:entityvo分开。很多项目图省事,直接拿实体类返回给前端,结果密码字段全暴露了,还不方便加一些额外字段(比如车辆图片的完整地址)。分开写虽然多几个类,但后期改接口的时候就知道有多爽了。

3. 数据库设计:订单表是整个系统的定海神针

3.1 三张核心表,把主体数据落稳

数据库设计的原则是先定核心实体,再扩展辅助表。汽车租赁系统的核心实体就是三个:用户、车辆、订单。辅助表可以有公告、车型分类等,但都是可选项。

用户表user,重点字段如下:

  • id:主键,自增
  • username:用户名,唯一索引
  • password:加密后的密码,不要存明文
  • phone:手机号
  • id_card:身份证号(做毕设可存可不存,留着体现“租赁需要实名”的业务合理性)
  • status:状态,1正常,0禁用
  • deleted:逻辑删除标记,0未删,1已删

车辆表car,重点字段如下:

  • idcar_no(车牌号,唯一)、brand(品牌)、model(车型)
  • price_per_day:日租金,用decimal(10,2)
  • deposit:押金,用decimal(10,2)
  • status:车辆状态,1可租,2已租,0下架
  • image:车辆图片地址
  • description:车辆描述,TEXT类型

订单表order,这张表是核心中的核心:

  • idorder_no(订单编号,可以用时间戳加随机数生成)
  • user_id:用户外键
  • car_id:车辆外键
  • plan_begin_time:计划取车时间
  • plan_end_time:计划还车时间
  • actual_end_time:实际还车时间(还车时更新)
  • total_price:总费用,decimal(10,2)
  • deposit:订单当时的押金额度(从车辆表冗余拷贝过来,因为车以后可能调价)
  • status:订单状态,0待审核,1已通过,2已拒绝,3租赁中(已取车),4已完成(已还车结算),5已取消
  • audit_remark:审核备注,管理员拒绝时填
  • create_timeupdate_time:创建和更新时间

这里有一个容易被忽略的细节:订单里的depositprice_per_day为什么要冗余一份?因为车辆表里的价格是可以变动的,而订单一旦生成,费用就应该锁定。如果不冗余,过几天车辆调价了,这笔订单的费用就算不清楚了。这是很典型的业务设计考虑。

3.2 状态流转图:订单的6个状态怎么走

我用文字把状态流转理一遍:

  • 用户提交订单 →待审核(0)
  • 管理员审核通过 →已通过(1),同时车辆状态改为已租(2)
  • 管理员审核拒绝 →已拒绝(2),订单流程结束
  • 用户取消 →已取消(5),只能在待审核(0)状态下操作
  • 用户取车(取车动作可由管理员确认)→租赁中(3)
  • 用户还车,管理员确认 →已完成(4),同时车辆状态改回可租(1)

这个状态机的核心约束有两条:

第一,待审核状态是取消订单的唯一起点。已经通过的订单不能随意取消,只能走完租赁流程。这个约束要在Service层做校验。

第二,订单状态和车辆状态必须联动变更。审核通过时改车为已租,还车完成时改车为可租。这一步需要加事务,否则会出现订单是已通过、车却是可租的不一致状态。后面第6章我还会专门讲事务的坑。

3.3 状态字段用数字还是字符串

照着上面写就好。数据库里存整数,代码里定义一个OrderStatusEnum枚举类,把每个数字对应的含义写清楚。这样数据库查询高效,代码可读性也好,前后端约定的字段值就是整数。用字符串pendingapproved也不是不行,但会白白增加存储成本和比较开销,没必要。

建表时给order表的user_idcar_id加上普通索引,给car表的brand加上普通索引。这样分页查询和筛选时能走索引,虽然毕设数据量小可能感觉不到差异,但评阅老师问到索引设计时你能答出来,这是加分项。

4. 核心模块实现:从车辆发布到还车结算的完整闭环

4.1 前端方案怎么选,这是很多人的盲区

先解决一个前置问题:这个系统要不要做前后端分离?

我观察到的现象是,很多同学被“前后端分离”这四个字绑架了,明明没有Vue基础,硬要搭一个Vue3 + Element Plus的前端,结果光是npm依赖和跨域问题就耗了三四天。

我的建议很直接:

  • 如果你前端基础弱,用Thymeleaf 服务端渲染,这是最稳的方案。SpringBoot自带模板引擎,能直接在HTML里写Java语法一样的东西,不用跨域、不用考虑token传参,代码量最少。
  • 如果你想界面好看一点,用Bootstrap + jQuery + Ajax,后端只提供JSON接口。前端页面还是放在static目录下,用Ajax请求后端接口。这种方式前后端也算分离,但没有工程构建的复杂度。
  • 如果你Vue有一定基础,或者想挑战一下,再考虑真正的前后端分离:Vue单独工程,通过反向代理或CORS解决跨域,登录用JWT。做成了是加分项,做不成就会拖累进度。

对于大多数希望顺利毕业的同学,我推荐第二种或第三种之间的平衡:后端统一返回JSON,前端用Bootstrap写页面。这样后端接口可以单独用Postman测,前端页面也不会太丑。

4.2 车辆管理:管理员的核心战场

管理员的车辆管理,本质是对car表做增删改查。但有几个细节需要注意。

新增车辆时,车辆图片是一个处理难点(我一开始是后端只接收图片地址,让管理员填一个外链URL,这样最简单。如果想让体验更好,就用MultipartFile上传到本地目录,再把访问地址存数据库。Upload File 保存路径要用配置项,不能写死)。

车辆列表分页查询,用MyBatis-Plus的分页插件:

@GetMapping("/admin/car/list") public Result carPage(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<Car> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Car::getBrand, keyword) .or(StringUtils.hasText(keyword)) .like(StringUtils.hasText(keyword), Car::getModel, keyword); wrapper.orderByDesc(Car::getCreateTime); Page<Car> page = new Page<>(pageNum, pageSize); return Result.success(carMapper.selectPage(page, wrapper)); }

这里用LambdaQueryWrapper而不是QueryWrapper,好处是类型安全,字段名写错了会在编译期就报错,而不是运行期才炸出来。车辆上下架,直接改status字段就行,核心是把状态值约定好。

4.3 用户下单:并发校验这一行代码是灵魂

用户下单的核心逻辑分三步:校验、写订单、改车辆状态。对于毕设来说,最容易写出问题的是校验这一步。

给大家看一个我把并发校验做对的版本:

@Transactional(rollbackFor = Exception.class) public Result createOrder(OrderCreateDTO dto) { // 1. 校验车辆存在且可租 Car car = carMapper.selectById(dto.getCarId()); if (car == null || car.getStatus() != 1) { throw new BizException("车辆不存在或已被租出"); } // 2. 生成订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setCarId(car.getId()); order.setPlanBeginTime(dto.getPlanBeginTime()); order.setPlanEndTime(dto.getPlanEndTime()); order.setDeposit(car.getDeposit()); order.setStatus(0); orderMapper.insert(order); // 3. 将车辆状态从可租(1)改为已租(2) int rows = carMapper.update(null, new LambdaUpdateWrapper<Car>() .eq(Car::getId, car.getId()) .eq(Car::getStatus, 1) .set(Car::getStatus, 2)); if (rows == 0) { throw new BizException("车辆已被抢租,请换一辆"); } return Result.success(order.getId()); }

请特别注意第3步这一段。如果按很多人的习惯写法,先selectById查到车辆,判断是可租,然后updateById改状态,这在单线程下没有问题,但两个用户同时请求时,两个请求都能查到同一个“可租”状态,然后都执行更新,就会出现超租(超卖)。解法是把这个状态判断放进update的 where 条件里,用数据库的行锁保证原子性——更新影响行数为0,说明条件不满足了,直接抛业务异常。

这个点如果能在答辩时讲出来,是很扎实的一个亮点。

4.4 审核与还车结算:费用计算必须在后端做

管理员审核订单时,通过就把状态改成已通过,并确保车辆状态已经是已租;拒绝则填备注并改成已拒绝。这里同样要有事务和状态校验。

还车结算是整个系统业务味最浓的部分。我的结算规则是这样设计的:

  • 租用天数为计划还车时间减去计划取车时间,按天向上取整
  • 基础费用 = 日租金 × 租用天数
  • 若实际还车时间晚于计划还车时间,超时部分按小时计费,每小时费用 = 日租金 ÷ 24
  • 总费用 = 基础费用 + 超时费用,押金在还车后自动退回(毕设里标记一下即可)

计算金额时必须用BigDecimal,绝不能用double。浮点数运算在金额场景下会出精度问题,比如0.1 + 0.2在二进制浮点里并不等于0.3。用上BigDecimal并用ROUND_HALF_UP保留两位小数,才是规范做法。

public BigDecimal calculateTotalPrice(BigDecimal pricePerDay, LocalDateTime planBegin, LocalDateTime planEnd, LocalDateTime actualEnd) { // 租用天数:向上取整 long days = ChronoUnit.DAYS.between(planBegin, planEnd); BigDecimal basePrice = pricePerDay.multiply(BigDecimal.valueOf(days)); // 超时费用:晚还部分按小时计算 BigDecimal totalPrice = basePrice; if (actualEnd != null && actualEnd.isAfter(planEnd)) { long hours = ChronoUnit.HOURS.between(planEnd, actualEnd); if (actualEnd.plusHours(-hours).isBefore(planEnd)) { hours = hours + 1; // 不足一小时按一小时算 } BigDecimal hourPrice = pricePerDay.divide(BigDecimal.valueOf(24), 2, RoundingMode.HALF_UP); totalPrice = basePrice.add(hourPrice.multiply(BigDecimal.valueOf(hours))); } return totalPrice.setScale(2, RoundingMode.HALF_UP); }

这段代码里,超时部分我把“不足一小时按一小时算”的规则也写进去了。这种细节平时测试不容易发现,但评阅老师问起来,你能说出自己的计费规则,就已经说明你懂业务了。

5. 技术亮点集中落实:登录鉴权、统一返回体和自动化特性

5.1 统一返回体和全局异常,代码立刻有了工程味

我见过不少项目,Controller返回什么类型的都有,有的返回Map,有的直接返回实体类,有的返回String。这样写一时爽,前端对接的时候就要哭了。

我习惯定义一个统一返回体Result<T>,包含codemessagedata三个字段,再配一个静态方法:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }

配合全局异常处理@RestControllerAdvice,业务层直接throw new BizException("车辆不存在"),前端就能拿到统一的错误结构。Controller里不再出现try-catch,代码清爽很多。这是很低成本的工程化优化,但对项目的可维护性提升是立竿见影的。

5.2 JWT + 拦截器:前后端分离下的登录状态管理

登录方式现在的主流选择是JWT。原理不复杂,就是一个包含用户信息的Base64编码串,用密钥做签名,每次请求时带在请求头里,后端验签后就能知道是哪个用户。

JWT的特点是服务端无状态,所以后端不需要存session。对于毕设来说,需要处理的完整链路是:

  1. 用户登录成功后,根据userIdusername生成token返回给前端
  2. 前端把token存起来,每次请求放在Authorization请求头里
  3. 后端自定义一个拦截器,拦截/api/**下的请求,除了登录、注册、车辆列表这几个白名单接口
  4. 拦截器里解析token,如果解析失败或过期,直接返回401未登录的JSON

关键代码在拦截器里:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}"); return false; } try { Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { response.setStatus(401); response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"message\":\"登录已过期\"}"); return false; } } }

解析用户信息这一步,我习惯用request.setAttribute存起来,Controller里再从request里取出userId。相比用ThreadLocal,这种方式更简单,写起来也不容易错。

5.3 MyBatis-Plus 三板斧:逻辑删除、自动填充、分页插件

MyBatis-Plus是毕设提速的神器,但有三个配置你得知道,不然用起来会有“怎么不生效”的困惑。

逻辑删除是第一个。在实体类的deleted字段上标注@TableLogic,BaseMapper的删除操作会自动变成更新操作,查询时自动带上deleted=0的条件。这样用户删除后还能在后台看到痕迹,不会真的把数据删没。配置逻辑删除后,注意所有自定义SQL里也要手动加条件,否则会有隐患。

自动填充是第二个。在create_timeupdate_time字段上标注@TableField(fill = FieldFill.INSERT)@TableField(fill = FieldFill.INSERT_UPDATE),再实现一个MetaObjectHandler的配置类,插入和更新时自动填充时间。省去每次手动 set 时间的步骤。

分页插件是第三个。MyBatis-Plus的分页功能不是默认开启的,要在配置类里注册:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(200L); interceptor.addInnerInterceptor(pagination); return interceptor; } }

不加这个Bean,selectPage执行后其实查的是全表数据,不会真正分页。这个坑我见过不少同学踩,特意提出来。

5.4 密码加密:BCrypt,别再用MD5

账密认证场景下,MD5和SHA这类哈希算法是不合适的。它们速度快、无盐值,彩虹表一查一个准。正确做法是用BCrypt,Spring Security里面就带了BCryptPasswordEncoder,单独引入spring-security-crypto依赖不用整个引入Spring Security,也能用。

BCrypt最大的特点是每次加密同一个明文,生成的密文都不一样,因为它内部自动加盐。验证时matches(明文, 密文)方法判断是否匹配。注册时加密存入,登录时匹配判断。

实际项目中我会在配置类里注册一个PasswordEncoder的Bean,然后用它来做注册和登录的加解密。这个点算是最容易演示的答辩亮点之一——你把密文展示给老师看,说明你考虑过真实系统的安全问题,比用明文存储的强太多。

6. 编码阶段最容易踩的六个坑,每个都真实发生在我调试过程中

6.1 图片上传后路径失效,刷新就404

图片保存到本地目录后会遇到一个经典问题:数据库存的是D:/upload/car/1.jpg这样的绝对路径,结果前后端一部署,路径根本访问不了。

我的解法是,数据库中只存相对路径/upload/car/1.jpg,然后在配置类里把本地磁盘目录映射成虚拟路径:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${carrental.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadDir + "/"); } }

这样前端只认/upload/xxx.jpg,底层文件放在哪里无所谓。部署到服务器时,只用改配置里的路径就行,代码不用动。简单有效。

6.2 LocalDateTime 前后端显示得一塌糊涂

Java 8的LocalDateTime和前端JS的日期格式默认对不上,经常出现前端收到一串数字(时间戳)或者“T”字符分隔的格式。解决起来很简单,在配置类里统一注册一个Jackson的序列化器,或者直接在实体类时间字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

我两种方式都用过,更推荐在配置类里全局配置,这样实体类上不用每个都加注解。

6.3 订单状态和车辆状态不同步,事务没生效

一个典型的“改订单状态成功,改车辆状态失败”场景。两个写操作必须在一个事务里,要么同成功,要么同失败。

容易踩的坑是@Transactional加在了私有方法上,或者同一个类内部调用this.xxx()方法时事务失效。Spring事务是基于代理实现的,内部调用代理不生效。

正确的打开方式是:

  • @Transactional加在 public 方法上
  • 事务方法由外部调用,不要同类内部自调用

如果遇到自调用场景,把事务方法拆到另一个Service类里,或者用AopContext.currentProxy(),后者毕设阶段不用深究,知道拆类就好。

6.4 分页插件配置了还是不生效

很多人配好了MybatisPlusInterceptor但还是查出全表,常见原因有两个:

一是版本不兼容。引入的MyBatis-Plus版本太老,分页插件类路径变了。3.5.x之后统一用MybatisPlusInterceptor,旧的用法已经弃用。

二是SQL里自己写了limit或者在XML里没有传递Page参数。分页生效的前提是Mapper方法第一个参数是Page对象,或者在XML里用IPage作为参数。不要自己在SQL里写死limit。

6.5 跨域拦截器报错:OPTIONS请求被卡住

前后端分离后,页面第一次发请求时会先发一个OPTIONS预检请求。如果拦截器把OPTIONS直接拦了,跨域错误层出不穷。

处理方式在前面JWT拦截器的代码里有,对OPTIONS请求直接放行。另外还需要一个 CORS 全局配置:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

把这两个点都处理好,跨域问题基本就消失了。

6.6 Lombok 装上却没生效

如果实体类上写了@Data,但代码里找不到 getter/setter 方法,大概率是IDEA的 Lombok 插件没启用,或者项目的 Annotation Processing 没打开。

结合IDEA的路径:Settings → Build, Execution, Deployment → Compiler → Annotation Processors,勾选Enable annotation processing。新版IDEA通常会询问是否启用插件,老版本需要手动确认。这个问题不大,但每次遇到都会让人莫名烦躁。

7. 答辩实战:把项目讲清楚比把项目写出来更重要

7.1 开场三分钟,按这个顺序讲不慌

很多同学答辩时喜欢从登录页面讲起,一页一页点过去,结果讲了五分钟老师还不知道你的系统整体是什么样。我的建议是开场按下述节奏来:

首先用一句话概括系统:这是一个基于SpringBoot的汽车租赁管理系统,面向管理员和普通用户两类角色,实现了从车辆发布、用户下单、订单审核到还车结算的完整租赁闭环。

然后快速画一张简单的业务流程图或状态流转图,不用太精细,目的是让老师建立全局认知。

接着讲技术架构:后端用SpringBoot,持久层用MyBatis-Plus,数据库MySQL,鉴权用JWT。如果用了Redis或Vue,在这时一并带出来。

最后演示核心功能时,挑三个链路讲,不要零散点按钮:

  • 用户注册登录 → 浏览车辆 → 下单 → 管理员审核 → 还车结算
  • 管理员新增车辆 → 上下架
  • 用户取消订单或管理员拒绝订单的异常流程

按链路演示,会让人觉得你对自己的系统有完整把握。

7.2 高频追问Top 8,提前把答案准备好

我把评阅老师最爱问的问题整理成人话版答案,你最好能脱稿回答:

为什么用SpringBoot而不是SSM?SpringBoot简化了配置,内嵌Tomcat能直接打包运行,自动配置帮我们省掉了大量XML配置,让开发者更专注业务。这也是当前企业主流的后端框架。

SpringBoot自动配置的原理是什么?@SpringBootApplication包含@EnableAutoConfiguration,启动时根据META-INF/spring.factories里的配置类,结合当前classpath下的依赖,按条件装配Bean。

JWT和Session有什么差别?Session存在服务端内存里,分布式场景需要共享存储;JWT服务端不存状态,每次请求通过验签来认证,适合前后端分离和水平扩展。但JWT也有缺点,服务端无法主动吊销,所以过期时间不能设太长。

订单状态是怎么管理的?用状态字段加枚举,定义了6个状态并按流程转移。状态变更时会加事务,保证订单状态和车辆状态一致。

并发下同一辆车被两个用户同时下单怎么办?我用的是条件更新UPDATE car SET status=2 WHERE id=? AND status=1,利用数据库行锁保证只有一个请求能更新成功。

数据库有哪些索引,为什么建这些?用户表的用户名字段建了唯一索引,订单表的用户id、车辆id建了普通索引,车辆表的品牌建了普通索引。查询驱动的场景下,这些索引能有效减少回表次数。

分页查询是怎么实现的?MyBatis-Plus的分页插件,底层是拦截器在SQL执行前把Page参数解析成limit语句,自动帮我们拼分页。

项目做了哪些测试?核心接口用Postman做了完整的功能测试,覆盖正常流程和主要异常流程,比如重复下单、取消等。如果时间允许可以提一句用Swagger统一了接口文档。

这8个问题答好了,基本就能稳住了。

7.3 演示环节,三个容易翻车的细节

演示账号一定要提前准备好。管理员和普通用户各准备一个,密码设置成简单的,提前登录一次,避免现场输入错误或者账号被禁用。

数据库要提前启动好,SQL脚本重新执行一遍,保证数据干净。演示的时候在里面故意留一辆可租的车和一个待审核的订单,这样流程能一气呵成。

如果演示过程中代码报错,千万不要当着老师的面改代码。先深吸一口气,看错误信息,如果是环境问题,直接说“这个接口我在单元测试里验证过,稍等我看下环境状态”。如果是业务问题,诚实说“这里我在开发时可能短路了,我的设计预期是XX”,也比愣在原地强。

最后再分享一个小建议。源码拿到手之后,别急着跑起来,先用一个下午把三样东西各读两遍:建表SQL、订单状态流转和核心Service。能随手画出订单状态图,能在黑板上写出订单表字段,这比任何润色话术都好用。毕设源码不是什么玄学,它就是一套带着业务逻辑的代码,你顺着业务走一遍,自然就通了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 18:13:33

深度优先搜索(DFS)在全排列问题中的应用与实现

1. 全排列问题与深度优先搜索的关系 全排列问题是计算机科学中一个经典的基础算法问题&#xff0c;它要求给定一组不重复的元素&#xff0c;输出所有可能的排列组合。比如对于[1,2,3]&#xff0c;它的全排列包括[1,2,3]、[1,3,2]、[2,1,3]、[2,3,1]、[3,1,2]、[3,2,1]这6种情况…

作者头像 李华
网站建设 2026/9/10 18:09:51

JAVA计算机毕设之基于 SpringBoot+Vue 的面向高校的课程质量评估系统的设计与实现 基于 SpringBoot 与 Vue 的课程评价管理(完整前后端代码+说明文档+LW,调试定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/10 18:09:46

西门子S7-1500与V90伺服混合编程实战

1. 项目背景与核心需求 在工业自动化控制领域&#xff0c;西门子S7-1500 PLC与V90伺服系统的组合已成为运动控制的标准解决方案。这个项目要解决的核心问题是&#xff1a;如何利用TIA Portal&#xff08;博途&#xff09;平台&#xff0c;将SCL结构化文本与梯形图&#xff08;L…

作者头像 李华
网站建设 2026/9/10 18:07:55

大模型训练监控指南:从Loss到Grad Norm的异常诊断与干预

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 18:06:37

CANN/GE ACL算子队列配置API

aclopSetMaxOpQueueNum 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、Ten…

作者头像 李华
网站建设 2026/9/10 18:06:07

CANN/GE逻辑流分配信息

LogicalStreamAllocationInfo构造函数和析构函数 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。…

作者头像 李华