SpringBoot校园拼车系统:从数据库设计到经纬度匹配实战 简介这是一份基于SpringBoot的校园拼车系统毕业设计完整资料包面向计算机专业学生、毕业设计者及SpringBoot框架初学者。资源包含系统源代码、MySQL数据库脚本和毕业设计论文完整覆盖需求分析、系统设计、编码实现、数据库管理至论文撰写的全流程涉及用户管理、车辆信息、路线发布、预约拼车、订单评价等典型模块。压缩包内含201个文件整体大小约6.04MB主要文件包括96个Java后端源码、34个HTML页面、14个XML配置、14个JavaScript脚本和8个CSS样式另有SQL建表脚本和Word版论文文档目录按工程结构组织便于直接导入IDEA启动。目前已有124人学习下载适合用来快速搭建毕业设计或作为SpringBoot实战入门项目。通过该资源可掌握RESTful接口开发、MyBatis配置、拼车匹配逻辑与数据库规范设计论文部分还提供了背景、需求、测试等章节范本能显著降低从零开始完成毕设的难度。1. 毕设选校园拼车系统SpringBoot 只是入场券真正的分水岭在业务建模每年毕设季校园拼车系统都是 SpringBoot 选题里的常青树但大部分作品停在「能登录、能发帖」的 demo 水平。这个标题真正的价值是逼你把一个完整业务闭环做出来用户注册登录、发布行程、乘客下单、司机确认、距离匹配、订单状态流转、互相评价外加一套能跑通的数据库设计和一篇能自圆其说的论文。它适合两类人一是想稳过答辩、不想在选题上冒险的普通选手二是打算拿这个项目去面实习、需要能讲清楚设计决策的进阶选手。两种目标的写法完全不同本文按「能扛住追问」的标准来讲。先泼一盆冷水这类系统翻车最狠的地方不在 SpringBoot 本身而在经纬度距离计算、订单状态一致性和数据库时区这三个坑上后面逐一拆开。2. 用 SpringBoot 2.7 MyBatis-Plus 组装最小骨架项目结构、依赖与第一个接口2.1 为什么我坚持用 2.7.x 而不是追最新版热搜里全是「springboot 版本太高」「springboot 配置」这恰恰是毕设翻车的第一源头。SpringBoot 3.x 从javax.*换到了jakarta.*网上能直接抄的教程大半是 2.x 写的你用一个 3.2 的新项目去跑 2.x 的代码import javax.servlet直接编译失败。对于要赶论文、要跑通演示的毕设场景稳定性压倒一切。我一般会固定用 SpringBoot 2.7.18 JDK 8 MyBatis-Plus 3.5.3 这套组合它有三重保障网上案例最多、答辩时老师大概率也熟、遇到问题 Stack Overflow 上一搜就有答案。项目结构上不要学网上的花哨分包就用最朴素也最容易被答辩老师认可的分层src/main/java/com/example/carpool/ ├── controller/ # 接收请求返回 JSON ├── service/ # 业务逻辑 ├── mapper/ # MyBatis-Plus 的 Mapper 接口 ├── entity/ # 数据库实体类 ├── config/ # 配置类WebMvc、跨域、拦截器 └── CarpoolApplication.java这个结构对应了经典的三层架构论文里画架构图也顺手。别把配置类、工具类堆在一个包底下答辩时被问到「你这个项目怎么分层」会非常被动。2.2 pom.xml 与 application.yml抄作业但要知道每个参数在干什么先看依赖。最小可跑的依赖清单就五个别多贴parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies !-- Web 启动器内嵌 Tomcat提供 REST 接口能力 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus 的 SpringBoot 启动器自带 CRUD 方法毕设神器 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency !-- MySQL 驱动注意 8.x 驱动类名和 URL 参数有变化 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Lombok用注解省掉 getter/setter 的模板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里三个参数值得在论文里写一笔spring-boot-starter-parent统一管理依赖版本避免你手动指定版本时和 SpringBoot 内置依赖冲突MyBatis-Plus 的版本要单独指定因为 SpringBoot parent 不管它MySQL 驱动 8.x 与 5.x 的连接参数差异很大下面配置里会体现。再看配置文件。这是全项目最容易出玄学问题的地方每个参数都有坑server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/carpool?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的数据库密码 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0serverTimezoneAsia/Shanghai不写的话数据库连接会报 CST 时区错误或者数据写入后时间差 8 小时这是毕设答辩的高频事故点。useSSLfalse是本地开发必须关掉的否则 MySQL 8.x 会提示 SSL 连接警告。allowPublicKeyRetrievaltrue是给 MySQL 8.x 用的不配会报Public Key Retrieval is not allowed。logic-delete-field: deleted是 MyBatis-Plus 的逻辑删除配置后面讲数据库设计时你会看到每张表都带deleted字段。2.3 第一个接口用 RestController 把用户注册跑通骨架搭好后的第一个动作不是写业务而是写一个最简接口验证「数据库 → MyBatis-Plus → Controller → 浏览器」这条链路是通的。以用户注册为例RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/register) public Result register(RequestBody User user) { // 1. 参数校验用户名和密码不能为空 if (user.getUsername() null || user.getPassword() null) { return Result.error(用户名和密码不能为空); } // 2. 查重用户名不能重复 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUsername, user.getUsername()); if (userService.count(wrapper) 0) { return Result.error(用户名已存在); } // 3. 密码加密后入库 user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes())); userService.save(user); return Result.success(user.getId()); } }逻辑说明第 1 步用最朴素的方式挡掉空参数毕设里引入 Validation 注解也行但手写校验在答辩时更容易讲清楚。第 2 步用 MyBatis-Plus 的LambdaQueryWrapper做条件查询User::getUsername是方法引用编译期就能发现字段名拼错。第 3 步的DigestUtils.md5DigestAsHex来自 Spring 自带的spring-core包不用额外引入依赖。注意一个细节返回给前端的是user.getId()而不是整个 user 对象避免把密码哈希值也带出去。参数说明PostMapping(/register)监听 POST 请求RequestBody告诉 Spring 把前端传来的 JSON 反序列化成 User 对象。前端调用时Content-Type必须是application/json用 axios 的话要显式设置headers: {Content-Type: application/json}这是前端同学最容易漏的一步漏了后台就收到 null。跑通这个接口后项目骨架就算立住了。接下来进入核心设计一套能支撑拼车业务的数据库。3. 校园拼车数据库设计五张表把用户、行程、订单、评价串成闭环3.1 从需求反推表结构而不是先建表再补需求很多毕设是反着来的先建了用户表和行程表写到订单功能时发现缺字段再回头 ALTER TABLE结果表和代码互相不认识。正确做法是从业务闭环倒推一个学生发布行程 → 另一个学生浏览并下单 → 司机确认 → 行程开始 → 行程结束 → 双方互评 → 同时为了沟通顺畅需要站内消息。这个流程里涉及的实体就是用户、行程、订单、评价、消息正好五张表。字段设计上我遵循三条原则能用tinyint表示状态就用tinyint能用decimal表示经纬度就用decimal所有表都带deleted逻辑删除标记和create_time创建时间。先看核心的用户表和行程表它们是整个系统的心脏CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT 密码MD5 或 BCrypt 哈希, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, student_no varchar(20) DEFAULT NULL COMMENT 学号, role tinyint(4) DEFAULT 0 COMMENT 角色0-乘客1-司机2-两者都是, deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除0-正常1-已删除, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;注意password字段我留了 100 的长度因为后面要换成 BCrypt 加密BCrypt 哈希串比 MD5 长得多。role用tinyint而不是字符串是因为角色只有有限的几种取值数字在代码里用枚举对应查询和索引都比字符串快。deleted字段配合 MyBatis-Plus 的逻辑删除配置所有查询会自动加上WHERE deleted 0这样用户注销只是改个标记不会真的丢数据论文里可以写「采用逻辑删除保护数据可追溯性」。行程表是拼车业务的枢纽字段设计直接决定匹配功能能不能做CREATE TABLE trip ( id bigint(20) NOT NULL AUTO_INCREMENT, driver_id bigint(20) NOT NULL COMMENT 司机用户 ID, origin_name varchar(100) NOT NULL COMMENT 出发地名称如东门, origin_lat decimal(10,7) NOT NULL COMMENT 出发地纬度, origin_lng decimal(10,7) NOT NULL COMMENT 出发地经度, dest_name varchar(100) NOT NULL COMMENT 目的地名称, dest_lat decimal(10,7) NOT NULL COMMENT 目的地纬度, dest_lng decimal(10,7) NOT NULL COMMENT 目的地经度, depart_time datetime NOT NULL COMMENT 计划出发时间, total_seats int(11) DEFAULT 4 COMMENT 总座位数, available_seats int(11) DEFAULT 4 COMMENT 剩余座位数, status tinyint(4) DEFAULT 0 COMMENT 状态0-招募中1-已满员2-已完成3-已取消, deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_time (status, depart_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT行程表;origin_lat和origin_lng用decimal(10,7)这是一个血泪经验经纬度如果存成float或double在第 7 位小数附近会有精度误差导致距离计算偏差几十米。decimal(10,7)能精确到厘米级别完全够用。联合索引idx_status_time是给第四章的距离匹配 SQL 用的按状态筛选招募中的行程再按时间范围过滤索引能大幅减少扫描行数。3.2 订单、评价、消息三张表的字段与状态机设计订单表是连接司机和乘客的桥梁状态字段是答辩老师最爱深挖的地方。用tinyint定义状态比用字符串好在两点存储小、比较快且不可能出现五花八门的自定义状态值。我会把状态定义写进代码的枚举类里而不是散落在业务逻辑中到处写魔法数字CREATE TABLE trip_order ( id bigint(20) NOT NULL AUTO_INCREMENT, trip_id bigint(20) NOT NULL COMMENT 行程 ID, passenger_id bigint(20) NOT NULL COMMENT 乘客用户 ID, seat_count int(11) DEFAULT 1 COMMENT 预定座位数, status tinyint(4) DEFAULT 0 COMMENT 状态0-待司机确认1-已确认2-已完成3-已取消, remark varchar(255) DEFAULT NULL COMMENT 乘客备注, deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, confirm_time datetime DEFAULT NULL COMMENT 司机确认时间, PRIMARY KEY (id), KEY idx_trip_id (trip_id), KEY idx_passenger_id (passenger_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT拼车订单表;trip_id和passenger_id上建单列索引是必须的因为订单查询永远是从「某个行程的所有订单」或「某个乘客的所有订单」这两个维度进。confirm_time单独留出来是为了后续做超时未确认自动取消的功能论文里可以写「预留自动化处理时间窗口」。评价表很简单但有个字段容易漏rating的取值范围要在代码层校验数据库层只保证 0-5 的整数CREATE TABLE review ( id bigint(20) NOT NULL AUTO_INCREMENT, trip_id bigint(20) NOT NULL COMMENT 关联行程, from_user_id bigint(20) NOT NULL COMMENT 评价人, to_user_id bigint(20) NOT NULL COMMENT 被评价人, rating tinyint(4) DEFAULT 5 COMMENT 评分 1-5, content varchar(255) DEFAULT NULL COMMENT 评价内容, deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评价表;消息表的结构几乎每个系统都一样不再赘述。这里要提醒一个关键设计决策不要用外键。所有表关联都靠业务代码维护理由有二一是 MyBatis-Plus 的 CRUD 不主动管理外键外键约束会影响删除操作二是毕设演示时经常要手动清库重来有外键的话删除顺序错了就报错非常尴尬。答辩被问「为什么不用外键」就答「外键约束在分布式场景下影响扩展性本系统在 service 层通过事务保证一致性」这个说法无懈可击。五张表建好后实体类用 MyBatis-Plus 的注解对应上即可。一个隐藏技巧用TableField(fill FieldFill.INSERT)配合 MetaObjectHandler 自动填充create_time让代码里完全不用手动 set 创建时间这也是论文里可以写的亮点。4. 把「顺路」从玄学变成可复现的 SQLHaversine 公式与范围预筛的匹配策略4.1 为什么不能在代码里遍历算距离校园拼车系统的核心功能是「查找顺路行程」。最粗暴的做法是把所有行程查出来在 Java 里用Math.sqrt挨个算距离过滤出 2 公里内的。这个做法在数据量 100 条时没问题但毕设答辩老师会追问一句「数据量到一万条呢」你就卡住了。因为每个乘客的行程匹配请求都要全表扫描一次还要算一万次三角函数响应时间随数据量线性恶化。正确的思路是把计算下推到数据库用 SQL 完成过滤。这里有两个可选方案MySQL 5.7 以上自带的ST_Distance_Sphere函数它直接返回球面距离单位是米精度对校园场景足够另一个是自己在 SQL 里写 Haversine 公式移植性更强。我推荐直接上ST_Distance_Sphere因为它是数据库原生函数走索引的机会更好代码也更短。4.2 一个可复现的匹配 SQL含边界参数说明匹配的逻辑拆成三步第一步按状态和时间过滤第二步用经纬度作矩形粗筛第三步用ST_Distance_Sphere精确计算距离。下面是核心 SQLSELECT t.id, t.origin_name, t.dest_name, t.depart_time, t.available_seats, -- 计算当前乘客位置到出发地的实际距离单位米 ST_Distance_Sphere(POINT(t.origin_lng, t.origin_lat), POINT(#{userLng}, #{userLat})) AS distance FROM trip t WHERE t.status 0 AND t.deleted 0 AND t.available_seats 0 AND t.depart_time BETWEEN #{startTime} AND #{endTime} -- 经纬度矩形预筛先缩小范围避免全表做球面计算 AND t.origin_lat BETWEEN #{userLat} - #{rangeLat} AND #{userLat} #{rangeLat} AND t.origin_lng BETWEEN #{userLng} - #{rangeLng} AND #{userLng} #{rangeLng} HAVING distance #{maxDistance} ORDER BY distance ASC LIMIT 20;逻辑说明WHERE里的状态、删除标记、座位数、时间范围四个条件能直接走idx_status_time联合索引先把扫描范围从全表缩小到「正在招募且时间合适的行程」。经纬度矩形预筛是第二道闸门#{rangeLat}和#{rangeLng}的计算方法是maxDistance / 111000因为纬度 1 度约等于 111 公里换算成米就是 111000。比如最大距离 2000 米rangeLat 2000 / 111000 ≈ 0.018经度同理但要在纬度方向上除以cos(纬度)修正校园场景范围小直接按 0.02 估算也行。HAVING distance #{maxDistance}是第三道精确计算注意HAVING只能过滤聚合函数或别名的条件这里的distance是 SELECT 里算出来的别名所以必须放HAVING而不是WHERE这是这条 SQL 最容易翻车的地方。最后按距离排序取前 20 条保证响应量可控。参数说明#{userLat}和#{userLng}是乘客当前或指定的出发位置#{startTime}和#{endTime}一般是当前时间到未来 2 小时内这个窗口大小可根据校园场景调整#{maxDistance}建议设 2000 米步行 25 分钟以内符合校园拼车「顺路」的直觉。4.3 索引与性能边界什么时候该上空间索引上面的 SQL 在几千条行程数据下性能没问题但有个隐藏瓶颈经纬度矩形预筛走不了普通 B-Tree 索引因为origin_lat BETWEEN ? AND ?和origin_lng BETWEEN ? AND ?是范围查询的叠加MySQL 优化器通常只选择其中一个条件走索引。数据量到十万级以上就需要考虑SPATIAL INDEX空间索引配合MBRContains函数了。MySQL 空间索引的用法是把经纬度合并成POINT类型的列存进Geometry字段然后建空间索引再用MBRContains做边界框快速过滤。代码长这样ALTER TABLE trip ADD COLUMN origin_geo POINT NOT NULL COMMENT 出发地空间坐标; UPDATE trip SET origin_geo POINT(origin_lng, origin_lat); ALTER TABLE trip ADD SPATIAL INDEX idx_origin_geo(origin_geo); -- 查询时用最小边界矩形过滤 SELECT t.id FROM trip t WHERE MBRContains( Envelope(LineString( POINT(#{minLng}, #{minLat}), POINT(#{maxLng}, #{maxLat}) )), t.origin_geo );这个方案我建议在论文的「系统优化」章节里写但不一定在演示时启用。因为空间索引要求 MySQL 版本支持且数据类型为 Geometry毕设环境不一定满足而且演示数据量小普通索引配合矩形预筛已经够快。写进论文能体现出你考虑过性能边界这比运行时复杂度的口胡有说服力得多。时间匹配上还有一个加分细节同一个司机不应该同时有两个在途行程所以发布新行程时要校验「该司机未来 2 小时内没有 status0 或 status1 的行程」。这个校验放在 service 层用LambdaQueryWrapper查一次即可防止数据库出现逻辑上不可能的数据答辩时这也是一个值得展开的「业务一致性」设计点。5. 从开发到答辩的避坑实录5 个我亲手踩过的高频事故5.1 数据库连不上时区与 SSL 的双重玄学现象项目启动时报Cannot create PoolableConnectionFactory或者连上了但所有时间字段差 8 小时。原因这是两个独立问题叠加。第一个是 MySQL 8.x 默认要求 SSL 连接第二个是 JDBC URL 里的时区参数缺失时驱动使用服务器默认时区而中国服务器一般配置的是 CST和本地 JVM 时区不一致后时间就会偏移。解决JDBC URL 上补全三个参数缺一不可useSSLfalse关掉 SSL 握手serverTimezoneAsia/Shanghai明确时区allowPublicKeyRetrievaltrue允许获取 RSA 公钥。写进application.yml后重启即可。这是我在帮同学调项目时遇到频率最高的问题光这一条就能拦下 30% 的启动失败。5.2 SpringBoot 版本太高javax 全家桶编译失败现象import javax.servlet.*报红编译直接失败网上搜到的解决方法是改jakarta.servlet但改了之后一大批第三方库跟着崩。原因SpringBoot 3.x 从 Java EE 迁移到 Jakarta EE包名从javax.*整体变成jakarta.*。网上大量现成代码是 2.x 时代的直接搬过来必然报错。解决毕设项目直接锁死 SpringBoot 2.7.18 JDK 8。如果你已经用 3.x 开了头两个选择一是全部改回 2.7.x 重建项目成本最低二是坚持 3.x但所有依赖都要确认兼容 Jakarta 命名空间例如springdoc-openapi要用 2.x 版本。我强烈建议选第一个毕设时间宝贵不值得在环境问题上耗一星期。5.3 前端传 JSON 后台收到 nullContent-Type 的隐藏杀手现象axios POST 请求发出去了Network面板显示payload里有数据但 Controller 里RequestBody User user收到的对象所有字段都是 null。原因axios 默认的Content-Type是application/x-www-form-urlencoded而后端RequestBody需要application/json。两边解析方式不匹配时Spring 会把请求体当成表单参数反序列化结果就是空对象。解决请求拦截器里统一设置 JSONaxios.interceptors.request.use(config { config.headers[Content-Type] application/json; return config; });这是一个非常典型的「前端和后端各看各的文档」的翻车现场。调试技巧是在application.yml里开启log-impl: org.apache.ibatis.logging.stdout.StdOutImpl在控制台观察 MyBatis 打印的 SQL 到底有没有拿到参数。5.4 经纬度距离算出来的结果全为 0现象ST_Distance_Sphere返回的distance全是 0 或者几百以内的小数跟实际距离完全对不上。原因数据库表里的经纬度字段建成了int或float116.407526存进int变成了116两点之间距离自然只有几百米。这类问题在表结构设计阶段埋下到写距离计算时才发现只能回头改表。解决建表时把经纬度字段的类型固定为decimal(10,7)并加COMMENT注明单位是度不是弧度。已经建错的表用ALTER TABLE trip MODIFY origin_lat DECIMAL(10,7)修正然后重刷数据。这一条充分验证了第三章建表时坚持decimal(10,7)的价值。5.5 订单状态乱跳缺少状态流转校验的后果现象乘客可以取消已经被司机确认的订单司机可以再次确认已完成的行程数据库里出现各种逻辑矛盾的状态组合。原因controller 里只写了「根据当前状态改成目标状态」的 UPDATE完全没有校验当前状态是否允许跳转到目标状态。解决在 service 层做一个状态机的集中校验。比如「取消订单」只允许从「待确认」「已确认」两个状态进入已完成和已取消的订单不允许再动。一个轻量写法是定义一个OrderStatus枚举枚举里放一个canTransitTo集合public enum OrderStatus { PENDING(0), CONFIRMED(1), COMPLETED(2), CANCELLED(3); private final int value; public boolean canTransitTo(OrderStatus target) { switch (this) { case PENDING: return target CONFIRMED || target CANCELLED; case CONFIRMED: return target COMPLETED || target CANCELLED; default: return false; } } }逻辑说明这个枚举把「什么状态能变成什么状态」收敛到一处controller 里改成先判断order.getStatus().canTransitTo(targetStatus)再执行更新。答辩时老师问「如何保证订单状态一致性」把这段代码亮出来比空谈理论有说服力得多。另一个好处是枚举的value和数据库tinyint一一对应杜绝了魔法数字。6. 从能跑到能演示把 JWT 登录、订单流程和检查清单做成答辩加分项骨架和核心业务都完成后距离一个「值得被老师记住」的毕设还差三个提亮点。第一个是登录方案别用 Session用 JWT。它的价值体现在两个地方一是无状态设计前端拿到 token 存到 localStorage后端用拦截器统一校验二是论文里能画一张「JWT 签发与校验时序图」来撑篇幅。实现上在pom.xml引入jjwt依赖登录成功后签发一个包含userId和role的 token写一个拦截器解析Authorization头放行或返回 401。要注意把 token 过期时间设成 2 小时刷新逻辑留给后续扩展。第二个提亮点是把订单流程走完整别停在「下单成功」就结束。我用的是一个三层的状态推进乘客下单后订单状态是「待司机确认」司机端看到后点「确认」变「已确认」同时行程的available_seats减去预订座位数行程状态从「招募中」自动变为「已满员」余座为 0 时行程结束后司机和乘客各自点「完成」订单变「已完成」双方进入评价页。这个流程里最出彩的动作是「确认订单时用事务同时更新订单状态和行程余座」代码里加上Transactional注解并在更新余座前用UPDATE trip SET available_seats available_seats - #{count} WHERE id #{tripId} AND available_seats #{count}这种带条件的形式防止超卖。把这个 SQL 讲清楚整个项目就有了灵魂。第三个提亮点是演示前的检查清单这一步决定了答辩现场是否翻车。我的习惯是三条第一确认 MySQL 服务已启动且carpool库存在先执行SELECT COUNT(*) FROM user验证连接第二固定端口server.port: 8080不要用默认的随机端口演示时告诉老师「访问 localhost:8080」第三用mvn clean package -DskipTests打成 jar 包用java -jar启动而不是依赖 IDEA 的 Run 按钮因为答辩现场可能会换电脑。这三条里封装 jar 这一步帮我在现场避免过一次事故——IDEA 的配置丢失导致启动失败换成 jar 包后反而一帆风顺。最后说一个我自己的血泪教训第一版系统里密码是明文存库的答辩时老师顺手查了一下 user 表当场指出严重安全问题。后来我换成了 BCryptpom.xml引入spring-security-crypto依赖注册时BCryptPasswordEncoder().encode(password)登录时matches校验改动量只有两个方法但安全级别的提升是质变的。这个教训我一直记得毕设不只是跑通代码每一个设计决策都要经得起追问。希望这篇笔记能帮你少踩几个坑把项目从「能跑」做到「能讲」预祝你答辩顺利。本文还有配套的精品资源点击获取