ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot的航旅APP核心链路:航班查询、库存控制与订单状态机设计

基于SpringBoot的航旅APP核心链路:航班查询、库存控制与订单状态机设计 1. 选题需求拆解三个标题其实指向同一套系统1.1 从标题关键词反推系统边界先说你拿到的这类题目。仔细看云上航空、云端飞航、航旅随心这三个词剥掉包装的外壳底子完全一样一个基于 SpringBoot 的航旅APP及配套管理后台。这类毕设标题在学校里非常常见或者说很多老师出题就是给一个业务场景再配上基于SpringBoot的XX平台剩下的系统边界要靠你自己去定义。我辅导过不少做这类题目的同学发现最常犯的错不是代码写不出来而是在动手前没有把系统到底要做什么想清楚结果做到一半发现功能太多毕业设计根本收不了尾。拿到这个题第一件事是拆出核心业务实体。航旅平台绕不开四样东西用户、航班、舱位库存、订单。围绕这四个实体展开的就是用户注册登录、航班查询、航班预订、订单支付、行程管理这几条线。APP端负责给乘客用管理端负责维护航班和舱位数据二者通过接口联动。有个容易忽略的点APP在毕设里的形态。你可以做 Android 原生Java/Kotlin、微信小程序也可以做 H5 套壳或者干脆用 Vue 写移动端网页然后通过 WebView 打包成 APP。学校验收时候只看是不是一个能跑的移动端应用所以完全没必要在客户端上追求复杂技术把精力留给后端业务逻辑才是稳妥路线。1.2 用户端与管理端的功能清单与优先级划分把功能范围控制住是毕设活下去的前提。我建议按下面这个表格来规划模块端模块具体功能优先级复杂度用户端APP账号体系注册、登录、修改密码、个人信息P0中用户端APP航班查询按起降城市、日期查询航班列表支持舱位筛选P0中用户端APP在线预订选择航班舱位、填写乘机人、提交订单P0高用户端APP订单管理待支付订单、查看订单详情、模拟支付、取消订单P0高用户端APP行程管理按时间线展示出行计划、倒计时、历史行程P1中用户端APP消息中心航班变动通知、订单状态通知P1低管理后台航班管理航班录入、班期规则、起降时间维护P0中管理后台舱位管理各舱位等级定价、余票库存调整P0中管理后台订单管理订单查询、改签/退票审核、状态干预P1中管理后台数据统计订单量、销售额简单图表P2中P0 是核心链路缺一个系统就跑不通P1 是亮点模块用来丰富功能描述P2 如果时间不够可以砍掉或者只做最简单的统计接口前端用柱状图糊一下。说实话评委老师更在意核心链路能不能讲清楚而不是你做了多少个花哨功能。1.3 为什么 SpringBoot 是这个题目的最优解很多同学会纠结要不要用 Spring Cloud、要不要上微服务。我的回答很直接毕设里面用 SpringBoot 单应用就够了微服务属于给自己挖坑。SpringBoot 的核心价值在于两点起步依赖和自动装配。起步依赖让你不用再为 Spring 和第三方库的版本兼容头疼引入spring-boot-starter-web就自带 Tomcat 和 Spring MVC自动装配则通过EnableAutoConfiguration帮你在引入依赖后自动配置好相应的 Bean比如引入 Redis 依赖后RedisTemplate就能直接注入使用。这种约定优于配置的思路完美契合毕设场景——你的核心目标是快速实现业务功能而不是花三周调各种 XML 配置。相比十几年前 SSHStrutsSpringHibernate时代那种面对一坨配置文件无从下手的状态SpringBoot 几乎把搭建项目的门槛降到了零。而且答辩的时候我使用 SpringBoot 简化了项目初始化流程通过自动装配减少了大量样板配置这句话本身就是个加分点老师爱听。2. 技术栈选型与数据库设计先把地基打稳2.1 一套不容易翻车的技术组合直接给结论这套组合我用过很多次稳定性很高层级选型版本建议备注后端框架SpringBoot2.7.18不要用 3.x见下方说明ORMMyBatis-Plus3.5.x内置分页插件、条件构造器数据库MySQL8.05.7 也行但 8.0 更省心缓存Redis6.x/7.x做航班查询缓存、Token 存储鉴权JWTjjwt 0.9.x无状态适合 APP 场景接口文档knife4j4.3.0方便自测和答辩演示前端APPAndroid 原生 或 Vue3H5—按个人熟悉程度选管理后台Vue3 Element Plus—或者直接用 Thymeleaf这里特别想提一句 SpringBoot 版本的问题。很多同学一上手就去创建最新版项目结果 SpringBoot 3.x 要求 JDK 17而部分同学的笔记本上装的是 JDK 8还有 MyBatis-Plus、druid 这些常用库对 SpringBoot 3 的兼容也各有各的坑。热搜词里springboot版本太高这个搜索词我猜就有一堆人踩了。稳一点的做法是直接用SpringBoot 2.7.18它是 2.x 系列的终点版本稳定、教程多、几乎不报错配合 JDK 8 完美运行答辩时完全够用。2.2 四张核心表的设计思路与建表语句数据库设计是整个项目的地基我的建议是不要照搬网上那种几十张表的完整设计按业务需要来。核心四张表用户表、航班表、舱位表、订单表。用户表不多说就是id, username, password, phone, real_name, id_card, create_time。密码记得用 MD5 加盐或者 BCrypt 加密别明文存储这个细节答辩时经常被问到。航班表是重点它和真实的航班排期还不一样。真实系统里航班有班期概念比如每天一班、每周一三五飞但毕设可以简化成每个日期一条航班记录也就是flight表里存的是具体某一天某一班飞机CREATE TABLE flight ( id BIGINT AUTO_INCREMENT PRIMARY KEY, flight_no VARCHAR(10) NOT NULL COMMENT 航班号如 CA1234, airline VARCHAR(20) NOT NULL COMMENT 航空公司, depart_city VARCHAR(20) NOT NULL COMMENT 出发城市, arrive_city VARCHAR(20) NOT NULL COMMENT 到达城市, depart_airport VARCHAR(50) NOT NULL COMMENT 出发机场, arrive_airport VARCHAR(50) NOT NULL COMMENT 到达机场, depart_time DATETIME NOT NULL COMMENT 起飞时间, arrive_time DATETIME NOT NULL COMMENT 到达时间, flight_date DATE NOT NULL COMMENT 飞行日期, status TINYINT DEFAULT 1 COMMENT 1正常 0取消, UNIQUE KEY uk_flight_no_date (flight_no, flight_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唯一索引(flight_no, flight_date)保证同一天同一航班号只有一条记录这是后面防重复预订的基础。舱位表用来存每种舱位的价格和库存CREATE TABLE cabin ( id BIGINT AUTO_INCREMENT PRIMARY KEY, flight_id BIGINT NOT NULL COMMENT 关联航班, cabin_class VARCHAR(10) NOT NULL COMMENT Y经济舱 F头等舱 C公务舱, price DECIMAL(10,2) NOT NULL COMMENT 票价, discount_rate DECIMAL(3,2) DEFAULT 1.00 COMMENT 折扣率, stock INT NOT NULL COMMENT 余票数, total_stock INT NOT NULL COMMENT 总座位数, UNIQUE KEY uk_flight_cabin (flight_id, cabin_class) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表在基本字段之外我强烈建议加一个快照设计。什么叫快照就是你下单时把航班号、起降时间、舱位等级、乘机人姓名身份证这些信息原样冗余一份存到订单里。这样之后航班改时间了、价格变动了都不影响已经出的订单也不影响你打印行程单。这个设计思路在答辩时说出去老师会觉得你考虑到了真实业务场景瞬间拉开和普通同学的距离。2.3 接口统一规范与 Token 鉴权接口设计直接决定前后端联调效率。我见过最痛苦的项目是每个接口返回格式都不一样前端解析字段快疯了。所以一开始就统一返回结构Data public class ResultT { private Integer code; // 200成功500失败401未认证 private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMsg(success); r.setData(data); return r; } public static T ResultT fail(String msg) { ResultT r new Result(); r.setCode(500); r.setMsg(msg); return r; } }APP 端的鉴权我推荐直接上 JWT。它的逻辑并不复杂用户登录成功后后端用密钥生成一个带过期时间的 Token 返回APP 把 Token 存在本地之后每次请求在请求头里带上Authorization: Bearer token后端用一个拦截器HandlerInterceptor校验 Token校验通过就把用户信息放进 ThreadLocal方便 Controller 直接取。为什么不用 Session因为 APP 不是浏览器Session 依赖 Cookie跨端体验不好而且分布式部署时 Session 还要额外做共享。JWT 天然无状态扩展性更强这个选型逻辑在答辩时能讲出一套完整的理由。再说一个热搜词里出现过的点全局过滤器处理上传 PDF 时的 XSS 攻击。这类安全问题放在毕设里其实是加分项。你可以在application.yml里配置一个过滤器对所有请求参数做script、onerror等关键字符的转义XSS 过滤器不需要很复杂用 OncePerRequestFilter 包装一层 HttpServletRequestWrapper 重写getParameter即可。虽然航旅系统里大多数情况不会有人恶意攻击你但答辩时能主动说出我做了 XSS 过滤说明你网络安全意识到位。3. 航班查询与预订订单业务里最考验细节的一环3.1 多条件航班查询与 Redis 缓存加速航班查询是用户打开 APP 后做的第一件事也是接口设计里最需要关注的性能点。查询条件一般是出发城市、到达城市、日期可选舱位等级和航空公司。对应的 SQL 不难但要注意索引SELECT f.id, f.flight_no, f.airline, f.depart_time, f.arrive_time, f.depart_airport, f.arrive_airport, c.cabin_class, c.price, c.discount_rate, c.stock FROM flight f LEFT JOIN cabin c ON c.flight_id f.id WHERE f.depart_city #{fromCity} AND f.arrive_city #{toCity} AND f.flight_date #{date} AND f.status 1 AND (c.cabin_class #{cabinClass} OR #{cabinClass} IS NULL) ORDER BY f.depart_time;注意depart_city、arrive_city、flight_date这三列是高频查询条件要建联合索引。另外时间字段的存储我强烈建议存DATETIME而不是字符串这样前端拿到手直接格式化还能避免时区换算的一堆麻烦比如起始日期当天 0 点 5 分的航班被字符串比较搞错位。既然用到了 Redis查询接口就可以加一层缓存。缓存 key 设计成flight:query:{from}:{to}:{date}:{cabinClass}value 存查询结果的 JSONTTL 设置 5 分钟即可。为什么要 5 分钟因为航班数据不是频繁变动的数据5 分钟的延迟用户根本感知不到而 Redis 缓存能扛住瞬时高并发也让你的项目在技术方案上有缓存这个层次。代码逻辑就三步先查缓存命中直接返回未命中查数据库再写缓存返回结果。有一点必须提醒管理后台修改了航班价格或取消航班后对应缓存要主动删除否则用户永远查到的是旧数据。用 Spring 的CacheEvict注解或者手动redisTemplate.delete(key)都行这个问题就是典型的缓存一致性问题答辩常问。3.2 预订事务与库存扣减优雅地防止超卖预订是整个项目里业务逻辑最重的接口流程是这样的用户提交订单 - 校验航班和舱位 - 扣减库存 - 创建订单记录状态待支付- 返回订单号。这里有两个问题必须解决一是扣库存和建订单要放在同一个事务里任何一步失败都要回滚二是并发场景下不能超卖也就是同一个舱位剩余 3 张票5 个人同时下单最后只能有 3 个人成功。用代码实现的思路有两条路悲观锁和乐观锁。悲观锁就是查库存的时候直接SELECT ... FOR UPDATE锁住这行其他人只能等锁释放。实现最简单但性能差点适合毕设场景。推荐代码示例Transactional public Order createOrder(Long userId, Long flightId, Long cabinId, ListPassenger passengers) { // 1. 悲观锁查询舱位 Cabin cabin cabinMapper.selectByIdForUpdate(cabinId); if (cabin.getStock() 1) { throw new BizException(该舱位已售罄); } // 2. 扣库存 cabin.setStock(cabin.getStock() - 1); cabinMapper.updateById(cabin); // 3. 生成订单... }selectByIdForUpdate是 MyBatis-Plus 里自定义 SQL配合Transactional确保整个操作原子性。乐观锁则是在cabin表加一个version字段更新时带上version条件UPDATE cabin SET stock stock - 1, version version 1 WHERE id #{cabinId} AND stock 0 AND version #{oldVersion}如果更新返回 0 行说明被别人抢了重新尝试即可。在答辩时把这两种方案都讲出来再说一句考虑到毕设并发量不大我最终选了悲观锁方案代码更直观如果做高并发优化会采用乐观锁既展示了知识面又体现了务实性。还要留个心眼处理接口幂等的问题。用户手快了点击两次提交订单结果下了两单这就是重复提交。最稳的办法是在订单表加一个order_no唯一索引用UUID去掉横线生成订单号插入时报主键冲突就说明重复下单另外前端也要做按钮置灰。这两层一叠加基本就防住了。3.3 舱位价格计算与订单快照设计价格这块别想复杂真实航空的动态定价是个大数据系统但毕设只要做好多舱位多价格就够了。简单策略就是每个航班下挂 N 个舱位经济舱/公务舱/头等舱各自有基础价和折扣率展示价格 基础价 x 折扣率。折扣率可以随时间变化比如提前 15 天订是 0.6 折临近起飞是 0.9 折逻辑写死在一个PriceService里方便答辩时讲解。下单时把价格算好存入订单表这里就引出快照表的价值了。订单表里冗余一份flight_snapshot字段JSON 类型存的是航班号、起降时间、舱位等级、单价、乘机人等信息的拼接串。用户查订单详情时直接读快照不需要实时去 join 航班表。这个设计的好处是订单的历史数据是冻结的哪怕航班改了时间用户看到的订单信息依然是下单那一刻的事实。用大白话讲就像你租房时签的合同房东之后涨价跟你没关系。乘机人信息录入时身份证号码要校验格式18 位正则手机号校验 11 位这是成本极低但体验提升明显的小细节。建议在 APP 端做一个常用乘机人管理用户把家人信息先录入下单时勾选即可既省事又显得功能完整。4. 行程管理从订单状态机到定时任务4.1 订单状态的定义与流转规则行程管理模块本质上是订单状态时间的展示。所以第一步把订单状态定义清楚。我用一个状态枚举来管理public enum OrderStatus { PENDING_PAYMENT(0, 待支付), PAID(1, 已支付), ISSUED(2, 已出票), CHECKED_IN(3, 已值机), COMPLETED(4, 已完成), CANCELLED(5, 已取消), REFUNDED(6, 已退票); private final int code; private final String desc; // getter... }状态流转规则要十分明确不能出现从已完成突然变成已取消这种诡异情况。合法的流转路径是待支付 -用户主动取消- 已取消待支付 -超时未支付定时任务取消- 已取消待支付 -模拟支付成功- 已支付/已出票已出票 -模拟值机- 已值机已值机 -飞行日期已过- 已完成已出票/已值机 -申请退票- 已退票在代码层面不要在 Service 里写一堆散落的 if else 判断状态而是封装一个orderStateMachine或者直接在枚举里加一个canTransitTo(target)方法统一校验。例如枚举里维护一个 Mapprivate static final MapOrderStatus, ListOrderStatus TRANSITIONS new HashMap(); static { TRANSITIONS.put(PENDING_PAYMENT, Arrays.asList(PAID, CANCELLED)); TRANSITIONS.put(PAID, Arrays.asList(ISSUED, REFUNDED)); // ... }每次状态变更走统一入口非法流转直接抛异常。答辩时你可以说这是借鉴了状态机设计模式保证了订单状态的安全性这一句话就能撑起一个加分点。4.2 行程按时间轴展示与倒计时计算行程管理在 APP 端呈现出来的样子应该是最近要飞的行程排在最前面每个行程卡片上有倒计时。后端接口设计上我建议提供两个接口查询待出发行程订单状态为已出票或已值机且起飞时间大于当前时间查询历史行程已完成、已取消、已退票待出发行程按起飞时间升序排列返回给前端时带上departTime时间戳。前端拿到后用本地时间计算倒计时格式可以做成距起飞还有 2 天 5 小时 30 分。这里有个坑千万不要在后端算了倒计时的字符串返回给前端因为前端页面停留期间时间一直在走如果后端只返回静态字符串倒计时不动用户会觉得系统坏掉了。正确的做法是后端只返回时间戳前端通过 JS 定时器每秒刷新一次倒计时。行程详情的页面可以展示一个时间轴从预订成功到支付完成到出票成功再到值机成功按时间顺序纵向排列。后端只需在订单状态变更时记录一条order_timeline表order_id, status, operate_time, note查询时按时间升序返回即可实现非常轻量。有了这个表订单详情的操作履历也顺便解决了一举两得。4.3 定时任务清理超时订单与航班变动提醒系统里有两类场景天然适合定时任务处理一是用户下单后一直不支付订单一直占着库存二是航班状态发生变化需要通知相关用户。第一类场景的处理逻辑不复杂。Spring 自带的Scheduled注解就能搞定假设设定 15 分钟未支付自动取消那么写一个定时任务每 5 分钟扫描一次订单表UPDATE orders SET status 5, cancel_time NOW() WHERE status 0 AND create_time DATE_SUB(NOW(), INTERVAL 15 MINUTE);这个 SQL 执行完还要把对应的舱位库存加回去因为之前的扣减操作要还回来。逻辑就是查出所有被取消的超时订单循环释放每个订单对应的舱位库存。注意这里也涉及事务一个订单释放失败不能影响其他订单。在阐述方案的时候你可以补充一句如果要做到更精准的延迟触发应该用 RocketMQ 的延迟消息或者 Redis 过期键监听但毕设用定时扫描已经足够这能体现你对方案边界有认知。第二类场景航班取消或者延误后要给已购票用户发通知。毕设实现最简单的方案不是上消息队列而是在消息中心表里插一条记录同时往用户的消息列表里写数据。可以做成管理后台修改航班状态时调用notifyService.flightChanged(flightId)这个 Service 查出所有关联该航班的未出行订单给每个用户生成一条站内消息。APP 端的消息中心拉取未读数量有红点提示即可。整体工作量很小但功能完成度高演示的时候效果很好。5. 从开发到答辩项目演示与高频追问整理5.1 开发环境的一致性问题做毕设期间我见过太多本地能跑一换电脑就报废的案例。环境统一是减少痛苦的唯一办法。我这里给一个完整的参考组合JDK 8 Maven 3.6.3用 IDE 内置也行 SpringBoot 2.7.18 MySQL 8.0 Redis 6.x Node 16前端。每个组件版本都要在项目文档里写清楚。关于项目构建工具很多同学纠结 Maven 还是 Gradle。我的建议是无脑选 Maven原因有两个一是 Maven 的依赖传递更直观出问题看报错更容易定位二是网上 SpringBoot 相关教程和 pom 配置几乎全是 Maven 的整合 MyBatis-Plus 时搜答案方便。热搜词里有人搜2020年 gradle 构建的 springboot 项目配置文件大概率是接手了早期模板项目然后踩了一堆 Gradle 和 SpringBoot 插件版本不兼容的坑这类问题完全可以从选型上规避。打包部署的问题也要提前想好。开发期你 IDE 里直接 Run 就行但演示前的打包必须跑通。在pom.xml里确保配置了 SpringBoot 的maven-plugin执行mvn clean package会打出一个可执行 jar。注意一个小坑如果项目里有前端静态资源比如管理后台的 dist打包顺序要先用npm run build构建前端再执行 Maven 打包把 dist 拷入src/main/resources/static这一步顺序乱了打包产物就会缺前端页面。5.2 演示环境与部署建议毕设演示最尴尬的场面是什么u 盘里的代码在讲台上打不开或者演示到一半 Redis 没启动直接白屏。我的建议是准备两套方案首选是本机演示。提前把所有服务启动好MySQL 服务、Redis 服务、后端 jar、APP 前端模拟器或真机。这里有个细节如果你用的电脑是公司或机房电脑MySQL 和 Redis 可能没装可以在答辩前一天把绿色版 MySQL 和 Redis 解压包准备在 u 盘里免安装启动的命令要自己先演练两遍比如 Redis 的redis-server.exe双击启动MySQL 的mysqld --initialize-insecure初始化流程。备选方案是 Docker 部署。如果你对 Docker 熟悉可以写一个简单的 docker-compose把 MySQL、Redis、后端三个容器编排起来。这里给一个参考 DockerfileFROM openjdk:8-jre WORKDIR /app COPY target/cloud-air-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]Docker 方案的好处是换任何一台有 Docker 的电脑都能秒级复现环境答辩时非常加分。但要注意不要为了演示 Docker 而把简单事搞复杂如果自己对 Docker 不熟老老实实用本机方案即可毕竟演示翻车比没有 Docker 更减分。5.3 答辩现场的高频问题与回答思路答辩环节老师大概率围绕技术选型和项目难点提问。我把出现频率最高的六个问题整理成回答思路你可以提前准备问题一SpringBoot 的自动装配原理是什么回答思路SpringBootApplication是组合注解核心是EnableAutoConfiguration。它在启动时扫描META-INF/spring.factories2.7 之前或者META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports2.7文件拿到所有自动配置类的全限定名然后通过Conditional系注解判断当前环境是否满足条件比如 Classpath 里有没有对应类满足才加载配置。这就是引入依赖即自动配置的原理。问题二为什么用 JWT 而不用 Session回答思路APP 不是浏览器Session 依赖 Cookie 不适用JWT 无状态、可跨端、易于扩展配合 Redis 做 Token 黑名单可以实现主动失效。但要主动提到JWT 无法在服务端主动销毁这是它的局限显得你的思考是辩证的。问题三你是怎么防止库存超卖的按照前面讲的悲观锁/乐观锁逻辑回答然后加一句我在订单号上加了唯一约束来保证幂等防止重复下单。老师基本就会点头。问题四Redis 缓存和数据库数据不一致怎么办回答思路修改数据库后主动删除/更新缓存缓存设置 TTL 过期兜底对一致性要求高的数据不缓存如支付结果。整个思路是最终一致性而非强一致性。问题五数据库表是怎么设计的为什么订单表要冗余航班信息把快照思路讲一遍强调保护历史订单不受后续数据变更影响。这是最容易展现设计水平的回答。问题六项目里最大的难点是什么不要回答没有难点也不要只回答登录。最好的答案是航班预订场景下的并发库存控制和订单状态一致性。把事务边界、库存扣减、状态机流转完整讲一遍展示你确实动手深入过。最后再分享一个我自己反复强调的技巧答辩演示前把测试环境的航班库存故意改成只有 1 张然后现场演示两个用户同时抢票展示只有一个能成功。这个操作视觉冲击力极强比你说一百句我处理了并发都管用。平时把这个场景录个视频放手机里万一现场网络出问题你还能兜底展示不至于冷场。做毕设这件事从头到尾最重要的能力不是堆新技术而是把一条核心业务链路想明白、做扎实、讲清楚这才是老师真正想看到的东西。
返回列表