ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java航空订票系统源码:状态机与并发控制实战

Java航空订票系统源码:状态机与并发控制实战 简介基于Java的航空订票管理系统设计源码是一套采用Swing与JDBC技术构建的完整桌面应用程序面向Java初学者、在校生以及需要快速完成课程设计或毕业设计的开发者。系统以MySQL 8.0为数据库通过JDBC完成数据访问覆盖登录、注册、主界面、退票、改签、账号管理等模块整体界面与业务逻辑分层清晰。压缩包共33个文件主要包含17个Java源文件、SQL数据库脚本、MySQL连接驱动JAR包、PNG界面预览图和使用说明并按src、database、img、lib等目录组织便于定位代码、脚本与依赖资源整体体积约2.87MB。导入IDE后可根据readme提示配置数据库并运行通过视图类与控制类代码可学习Swing事件监听、表格展示以及JDBC增删改查等典型实现。目前已有357人学习下载既适合理解小型管理系统的完整开发流程也可作为课程设计、毕业设计或自学练手直接运行和二次改造的基础项目。1. 基于Java的航空订票管理系统源码先别急着跑把状态机和并发想清楚航空订票管理系统在课程设计和毕业设计源码里和图书管理系统一样属于人手一份的常青树但真正能一次跑通的人不多。表面看业务只有查航班、下单、支付、退票四个动作一写就暴露三个硬问题同一个航班只剩一张票时怎么保证不被两个用户同时抢到支付回调重复到达时订单状态怎么保持住跨服务器部署后航班日期为什么总是差一天。这三个问题也是 Java 面试里被反复追问的数据一致性场景。这篇记录适合正在找 Java 课程设计案例源码、或者拿到一套源码不知道怎么改的读者我按能从零复现的路线把业务建模、核心代码、踩坑点和二次开发一起拆开。2. 航空订票的业务建模顺序实体关系、订单状态机与最小表结构2.1 实体梳理用户、航班、舱位、订单、乘客怎么关联大多数能搜到的航空订票管理系统源码表结构差别不大差别在订单和库存的处理方式。核心实体是五个用户、航班、舱位、订单、乘机人。航班是一次具体飞行比如 CZ3101 每天一班所以航班的粒度要落到“航班号 日期”舱位是按头等舱、公务舱、经济舱分开定价和分开计库存的订单是一次购买行为可以包含多个乘机人所以订单和乘机人必须拆成两张表不能把多个乘客名拼在一个字段里。实体关系可以这样定用户对订单是一对多订单对乘机人是一对多航班对订单是一对多航班对舱位是一对多。剩下一个容易漏的是支付记录支付回调是外部渠道反复请求的必须单独留一张表记录回调流水否则排错时只能翻日志。不少课程源码会把余票直接放在 flight 表上库存字段叫 tickets 或 surplus这样应付最简单的演示够用。一旦要做多舱位、改签、退票释放库存就会发现库存和航班耦合太紧。我一般建议把舱位独立出来用 flight_no depart_date cabin_level 做唯一键每行一个价格、一个总库存、一个余票数改签和退票都通过这张表加减余票。2.2 订单状态机从待支付到已出票再到已退票禁止任意跳转订票系统最容易被低估的是订单状态的管理。新手源码里 status 字段通常是 String想改成什么就改成什么退票完成后订单还能被支付回调改成已支付这种事在真实项目里会发生因为回调线程和用户操作线程是并发的。最小状态集建议这样定义PENDING 待支付、PAID 已支付、ISSUED 已出票、CANCELLED 已取消、REFUNDED 已退票。如果还有改签需求再加 CHANGE_PENDING 或不加改签用原订单取消加新订单落地的方案。当前状态触发事件结果状态约束PENDING用户取消CANCELLED释放占用的座位库存PENDING支付回调成功PAID校验金额一致回调幂等PAID系统出票ISSUED出票后不允许直接取消ISSUED申请退票REFUNDED释放座位库存登记退款PAID / ISSUED改签原单作废CANCELLED原座位释放新单重新扣库存代码里不需要引入复杂的工作流引擎只需要一个状态流转 Map 或者把校验写在 Service 入口。核心原则是状态更新语句必须带条件类似UPDATE orders SET status PAID WHERE order_no ? AND status PENDING受影响行数为 0 就说明状态已被别人改过直接拒绝。这个习惯能挡掉大部分并发状态错乱。2.3 用六张表把最小闭环跑出来建表 DDL 与字段取舍下面的 DDL 是我处理这类源码时习惯落地的版本用 MySQL 5.7 以上即可跑。注意几个选择金额用 DECIMAL(10,2) 而不是 float/double近距离见过用浮点算金额最后多出 0.01 的翻车现场密码字段注明用 BCrypt课程源码里常见的 MD5 只建议做演示日期用 DATE 和 DATETIME不要用 VARCHAR 存日期否则查询排序和时区转换都会很痛苦。CREATE DATABASE IF NOT EXISTS airline DEFAULT CHARSET utf8mb4; USE airline; CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 演示可用MD5正式建议BCrypt, real_name VARCHAR(32) NOT NULL, id_card VARCHAR(18) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE flight ( id BIGINT NOT NULL AUTO_INCREMENT, flight_no VARCHAR(10) NOT NULL, depart_city VARCHAR(32) NOT NULL, arrive_city VARCHAR(32) NOT NULL, depart_date DATE NOT NULL, depart_time VARCHAR(5) NOT NULL COMMENT 存字符串 07:30 便于展示, arrive_time VARCHAR(5) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0取消, PRIMARY KEY (id), KEY idx_route_date (depart_city, arrive_city, depart_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE flight_seat ( id BIGINT NOT NULL AUTO_INCREMENT, flight_no VARCHAR(10) NOT NULL, depart_date DATE NOT NULL, cabin_level VARCHAR(10) NOT NULL COMMENT F头等 C公务 Y经济, price DECIMAL(10,2) NOT NULL, total INT NOT NULL, remaining INT NOT NULL, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号防超卖, PRIMARY KEY (id), UNIQUE KEY uk_flight_date_cabin (flight_no, depart_date, cabin_level) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, flight_no VARCHAR(10) NOT NULL, depart_date DATE NOT NULL, cabin_level VARCHAR(10) NOT NULL, total_price DECIMAL(10,2) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT PENDING, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_passenger ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, passenger_name VARCHAR(32) NOT NULL, id_card VARCHAR(18) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE payment_log ( id BIGINT NOT NULL AUTO_INCREMENT, payment_no VARCHAR(32) NOT NULL, order_no VARCHAR(32) NOT NULL, amount DECIMAL(10,2) NOT NULL, channel VARCHAR(16) NOT NULL, status VARCHAR(16) NOT NULL, callback_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_payment_no (payment_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么不建物理外键约束课程源码经常遇到导入环境失败、联合查询报错的问题物理外键会在初始化数据和调整数据时添乱。这里用逻辑外键应用层保证 user_id、order_id 有意义数据库只加普通索引。这是从可用性出发的选择不是否定外键面试时能把这个理由说清楚反而是加分项。3. 跑通订票主流程的代码方案航班查询、下单扣库存、支付回调3.1 Maven 依赖与包结构先搭一个能被二次开发的骨架我接手源码的习惯是先确认技术栈。搜到的大多数老源码是 JSPServlet 或 SSM能跑但结构比较散下面按 Spring Boot MyBatis 这套最常用的改法讲如果你是传统 Servlet 版本把对应层换成 Filter 和 Service 即可状态流转的思路完全不变。先看 pom 的关键依赖。Spring Boot 用 2.7 这条稳定线MyBatis 集成用官方 starterMySQL 驱动用 8.x 新坐标。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 模块自带内嵌 Tomcatjar 包直接启动 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 与 Spring Boot 集成注意版本和 Boot 匹配 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies包结构按 Controller、Service、Mapper、entity 四层切这是最常见也是最好维护的划分。别把 SQL 写在 Controller 里也别让 Service 直接操作 Connection课程源码最需要重构的就是这一点。com.example.airline ├── controller # HTTP 接口层只做参数接收和结果包装 ├── service # 业务逻辑层事务和锁都放在这一层 ├── mapper # MyBatis 接口写数据库操作 ├── entity # 表对应的实体 └── common # 统一返回结果、异常、工具类很多源码的 Controller 里直接 new ServiceImpl没有接口和实现分离我一般建议保留接口原因不是形式主义而是后续做 AOP 事务、拦截器时会对 Spring 代理更友好排查问题也更简单。3.2 航班查询 SQL日期边界、余票判断和舱位筛选一起处理航班查询是订票系统的入口也是 Redis 缓存改造最容易下手的地方。常见做法是不带缓存的 SQL 直查后续再根据热点航线加缓存。注意日期条件要直接用depart_date #{departDate}不要写成depart_time LIKE 2025-0%这种字符串匹配日期列上有索引才能利用上。select idsearchFlights resultTypecom.example.airline.entity.FlightSeatVO SELECT f.flight_no, f.depart_city, f.arrive_city, f.depart_time, f.arrive_time, s.cabin_level, s.price, s.remaining FROM flight f JOIN flight_seat s ON s.flight_no f.flight_no AND s.depart_date f.depart_date WHERE f.depart_city #{departCity} AND f.arrive_city #{arriveCity} AND f.depart_date #{departDate} AND f.status 1 AND s.remaining 0 ORDER BY f.depart_time, s.price /select这条 SQL 的数据来自两张表为什么不直接查 flight_seat因为用户界面要展示航线信息和舱位信息join 是必需的。查询参数里 departDate 在 Java 侧用 LocalDate 类型传递MyBatis 会把 LocalDate 映射到 DATE 列时区问题在第 4 章单独说。排序用起飞时间和价格这个顺序对用户选票最直观。3.3 下单接口事务、行锁与乐观扣减的代码实现下单是订票系统最重要的一段代码库存判断和扣减必须是原子操作。常见做法是 SQL 里同时完成检查余票和扣减避免先 select 再 update 带来的超卖窗口。Service public class OrderServiceImpl implements OrderService { Transactional(rollbackFor Exception.class) public Order createOrder(CreateOrderReq req) { // 1. 价格不能信任前端传参要从库里查出来再计算 FlightSeat seat flightSeatMapper.selectSeatForUpdate( req.getFlightNo(), req.getDepartDate(), req.getCabinLevel()); if (seat null || seat.getRemaining() 0) { throw new BusinessException(该航班舱位已无余票); } // 2. 扣减库存行数不等于 1 说明余票刚好被抢完 int rows flightSeatMapper.deductWithCheck(seat.getId()); if (rows ! 1) { throw new BusinessException(手慢了余票刚被抢完); } // 3. 插入订单和乘机人信息 Order order buildOrder(req, seat); orderMapper.insert(order); orderPassengerMapper.batchInsert(order.getId(), req.getPassengerList()); return order; } }对应的 Mapper 扣减语句是乐观锁的核心一条 UPDATE 同时完成条件检查和数值变化。update iddeductWithCheck UPDATE flight_seat SET remaining remaining - 1 WHERE id #{id} AND remaining 0 /update为什么不是先查remaining 0再 update因为两个操作之间可能有别的线程已经扣过库存重新查到的余票是过期的。上面这条 UPDATE 依赖数据库行锁保证原子性同一个 id 行同一时刻只有一个事务能执行更新受影响行数返回 1 才代表扣减成功。这种写法比synchronized靠谱得多。synchronized 只能锁住当前 JVM 内的线程一旦应用部署多个实例、或者数据库和 Web 服务器分开部署锁就不生效了。乐观锁的 version 字段在多数课程项目里够用第 4 章会展开讲翻车案例。3.4 支付回调先查状态再改状态幂等就是这么来的支付网关回调的特点是不可靠可能重复推送可能延迟很久。处理回调的第一原则是幂等同一个回调报文处理两次和一次结果必须相同。大多数源码翻车就翻在回调直接 update status重复回调触发两次业务动作。Transactional(rollbackFor Exception.class) public void handlePayCallback(PayCallbackReq cb) { // 用订单号锁住订单行避免并发修改状态 Order order orderMapper.selectByOrderNoForUpdate(cb.getOrderNo()); if (order null) { throw new BusinessException(订单不存在); } // 已支付或已出票直接返回成功幂等关键点 if (PAID.equals(order.getStatus()) || ISSUED.equals(order.getStatus())) { return; } if (!PENDING.equals(order.getStatus())) { throw new BusinessException(订单状态不允许支付); } // 校验金额一致防止回调金额和订单金额不一致 if (order.getTotalPrice().compareTo(cb.getAmount()) ! 0) { throw new BusinessException(支付金额与订单金额不一致); } // 状态带条件更新受影响行数是 0 说明状态已被修改 int rows orderMapper.updateStatusIfPending(order.getId(), PAID, cb.getPaymentNo()); if (rows ! 1) { throw new BusinessException(订单状态更新失败); } paymentLogMapper.insert(cb); }selectByOrderNoForUpdate 是用FOR UPDATE把订单行锁住这是悲观锁思路适合订单这种单条数据的更新。如果不想用行锁也可以用乐观锁 UPDATE 的受影响行数判断两种都可以接受关键是支付回调的过程不能修改航班余票余票在支付成功后再扣容易出问题通常下单时就锁定库存支付失败或超时再释放。4. 避坑排查并发超卖、时区、编码和数据库连接池四个高频问题4.1 并发超卖最后一个座位被两个人同时订走现象数据库里剩余票数为 1同时提交两个订单请求两边都提示下单成功最后余票变成 -1 或 0但有两个订单都关联了同一个舱位。原因典型写法是先select remaining判断大于 0再执行update remaining remaining - 1。在并发下这两个步骤之间发生线程切换两个请求都读到了 remaining1然后各自扣减数据库层的行锁没能协调好因为它作用在两次独立 SQL 之间中间查询结果对另一个事务不可见。用 synchronized 也解决不了课程源码常常是单 Tomcat 实例看上去好用一旦部署多个实例就失效这是面试常问的点。解决把判断和扣减合并成一条 UPDATE用受影响行数判断。上面的 deductWithCheck 就是标准写法。更严格的做法是在 flight_seat 表上维护 version 字段每次扣减带上version #{expectedVersion}更新成功后 version 加一防止 ABA 问题。课程项目里WHERE remaining 0已经足够不用把乐观锁复杂化。4.2 航班日期差一天LocalDate 与时区的边界问题现象本地运行没问题部署到云服务器后用户查 5 月 1 日的航班页面显示的是 4 月 30 日的航班或者支付回调里的时间比数据库时间快了 8 个小时。原因MySQL 连接串里没设置时区驱动会读 MySQL 会话的 time_zone而云数据库实例经常默认 UTCJVM 默认 Asia/Shanghai两边一差就是 8 小时。另一种常见情况是代码里用new Date()然后通过 MyBatis 写入 DATETIME 列MyBatis 在驱动层做时间转换时又叠加一次时区偏移。解决JDBC URL 显式指定serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8表字段用 DATETIME 存储Java 侧用 LocalDateTime 和 LocalDate 映射不用 Date。查询航班日期时前端传“2025-05-01”这种字符串后端解析为 LocalDateSQL 里直接depart_date #{departDate}避免拼接 2025-05-01 00:00:00 AND 2025-05-02 00:00:00时又踩一次时区换算。4.3 中文乱码与绝对路径Windows 上源码跑不起来的第一原因现象Maven 编译报不可映射字符或者网页上中文全变成问号更隐蔽的是数据库里查出来是正常的页面显示乱码。原因多数课程源码在中文 Windows 上编写IDE 用 GBK 保存文件但 Maven 默认按平台编码编译Linux 部署时强制 UTF-8一编译就报错。数据库连接如果没设 characterEncodingMySQL 客户端默认用 latin1中文自然变问号。解决pom 里显式声明project.build.sourceEncodingUTF-8所有 Java 文件用 IDE 的 Convert to UTF-8 转一遍JDBC URL 加characterEncodingutf8建表语句统一CHARSETutf8mb4。还有一个容易忽略的是 Tomcat 的 URIEncodingSpring Boot 内嵌 Tomcat 默认 UTF-8传统外置 Tomcat 需要设URIEncodingUTF-8。另外源码里如果出现D:/xxx这样的绝对路径基本是作者本机的上传目录或配置文件路径先搜出来改成相对路径或者配置项。4.4 数据库连接池会话超时后第一个请求报连接异常现象系统刚启动一切正常过了半小时或几小时第一个请求报Communications link failure或Connection is not available刷新一次又好了。原因MySQL 默认 wait_timeout 是 8 小时空闲连接会被服务端断开但连接池不知道还在继续复用这条已经被服务端关闭的连接。老源码用 DBCP 或 C3P0 时如果没有配置连接验证就会出现这种间歇性断连。这类问题最迷惑人日志里看不到业务异常实际是连接池丢了一个死连接。解决用 HikariCP 时把maximumPoolSize设为合理值比如 10 到 20并把connection-test-query或validation-query配好Spring Boot 默认 HikariCP 会自动检测但如果源码里手动配过spring.datasource.type就要检查。老项目用 C3P0 的话设置testOnBorrowtrue虽然多一次验证查询、性能略有损失但对演示型系统来说稳定性优先。还有个小坑MySQL 8 的驱动类名和连接协议有变化源码里如果用老驱动连新数据库也会出现握手失败驱动版本和数据库版本尽量保持一致。5. 二次开发路线给现有源码加上“机票改签”功能的完整步骤5.1 面对黑匣子源码先从 Controller 到 Mapper 把调用链吃透拿到的源码通常不会带文档第一件事不是看业务代码而是搭一条调用链。做法是在 IDE 里搜RequestMapping或WebServlet找到接口入口然后顺着 Controller 方法进 Service再进 Mapper 接口最后看 Mapper XML 里操作的哪张表。把这条链写下来比漫无目的读源码高效得多。调用链节点找什么判断标准ControllerURL 路径、请求参数参数是否做了校验Service事务注解、是否有锁操作扣库存是否原子Mapper 接口方法名和参数SQL 和表名是否对应Mapper XML执行的 SQL是否用了 LIKE 拼日期对应表索引、字段类型是否能支撑业务扩展拿到源码后先跑一个最简单的接口比如查询航班列表断点打到 Service 里看它实际查了什么表。很多源码的实体类和表字段对不上或者 MyBatis 的 resultMap 字段名和数据库列名大小写不一致这类问题通过调用链一查就暴露。分析完调用链再判断改签功能要动哪几层Controller 加接口、Service 加业务、Mapper 加 SQL改动边界就清楚了。5.2 改签的状态迁移与数据变化原订单不能直接改航班改签比退票再买票复杂在需要保留历史。最常见的方案是原订单标记为已取消取消原因写“改签”然后创建一张新订单。这样用户历史记录和财务数据都对得上也避免在一张订单上频繁修改航班号导致审计混乱。数据项改签前改签后原订单状态ISSUEDCANCELLED备注改签原航班座位占用释放新订单无新建状态 PENDING 或 PAID新航班座位空闲扣减价格差异无差价记录有余退差如果只想让原订单支持一次改签也有方案只更新 orders 的 flight_no 和 depart_date加一个 change_source 字段记录原航班。但这种做法丢失了原始航班信息后续做统计和对账都难受。我更推荐原单取消加新单创建改签时把原订单的 flight_no、depart_date 存到一个 change_log 表里。5.3 改签接口实现校验、行锁、扣减与释放的顺序改签的核心代码必须保持固定的顺序锁定原订单、校验状态、锁定目标座位、扣减目标库存、释放原库存、插入新订单。顺序乱会导致库存不一致比如先释放再扣减扣减失败时两边库存都不对。Transactional(rollbackFor Exception.class) public Order changeOrder(ChangeOrderReq req) { // 1. 锁定原订单防止支付回调或退票同时操作 Order order orderMapper.selectByOrderNoForUpdate(req.getOrderNo()); if (order null) { throw new BusinessException(原订单不存在); } // 只有已出票状态允许改签这是状态机约束 if (!ISSUED.equals(order.getStatus())) { throw new BusinessException(当前订单状态不允许改签); } // 2. 锁定目标航班舱位判断余票 FlightSeat targetSeat flightSeatMapper.selectSeatForUpdate( req.getTargetFlightNo(), req.getTargetDate(), order.getCabinLevel()); if (targetSeat null || targetSeat.getRemaining() 0) { throw new BusinessException(目标航班已无余票); } // 3. 扣减目标库存 int rows flightSeatMapper.deductWithCheck(targetSeat.getId()); if (rows ! 1) { throw new BusinessException(目标航班余票扣减失败); } // 4. 原航班库存释放原订单标记改签取消 flightSeatMapper.release(order.getFlightNo(), order.getDepartDate(), order.getCabinLevel()); orderMapper.updateStatusWithReason(order.getId(), CANCELLED, 改签); // 5. 创建新订单价格按差价计算 Order newOrder buildChangeOrder(order, targetSeat, req); orderMapper.insert(newOrder); return newOrder; }参数说明selectByOrderNoForUpdate 使用悲观锁它会在事务提交前一直持有订单行的行锁挡住支付回调的并发操作release 方法是 update flight_seat set remaining remaining 1释放库存deductWithCheck 是前面提过的乐观扣减。事务注解必须加 rollbackFor Exception.class否则默认只在 RuntimeException 时回滚BusinessException 如果继承自 Exception 会导致库存扣了但订单没建成功。还有一个常见坑一个类内部方法调用 Transactional 方法事务会失效。比如 OrderServiceImpl 的 changeOrder 调用同一个类里的另一个 Transactional 方法Spring 代理不会拦截内部调用。解决办法是把扣减库存、插入新订单这些方法放到独立 Service 里或者直接在 changeOrder 方法上声明事务。6. 上线前的自测与部署把源码变成可演示的成品6.1 三个必须测的场景正常订票、库存为 1、支付回调重放给源码做验收不需要写一堆单元测试先手工把三个核心场景跑通。第一个是正常下单流程查航班、提交订单、模拟支付回调、查订单状态为已出票。第二个是并发边界把某个舱位余票改为 1开两个窗口同时提交订单预期其中一个提示余票不足数据库剩余票数为 0 而不是负数。第三个是支付回调重放同一个支付流水号连续回调两次订单状态还是已出票payment_log 表只有一条记录。# 查询航班 curl http://localhost:8080/api/flight/search?departCity北京arriveCity上海departDate2025-06-01 # 提交订单 curl -X POST http://localhost:8080/api/order \ -H Content-Type: application/json \ -d {flightNo:CZ3101,departDate:2025-06-01,cabinLevel:Y,passengers:[{name:张三,idCard:110101...}]} # 模拟支付回调连续执行两次验证幂等 curl -X POST http://localhost:8080/api/pay/callback \ -H Content-Type: application/json \ -d {orderNo:20250601001,paymentNo:PAY001,amount:1290.00}并发边界如果我一定要模拟就在本地用两个终端同时执行下单命令或者用一个 for 循环发 10 次请求看返回结果。对课程演示来说手工跑通这三个场景已经足够说明系统可用。6.2 打包与启动jar 包和 war 包的路径差别Spring Boot 项目用 jar 包启动最省事内嵌 Tomcat不需要单独装。传统 JSPServlet 源码打包出来是 war需要放进外置 Tomcat 的 webapps 目录。先看清源码的打包方式再决定用哪条命令。# Spring Boot 项目 mvn clean package -DskipTests java -jar target/airline-0.0.1-SNAPSHOT.jar --server.port8080 # 传统 JSP 项目 mvn clean package -DskipTests # 把 target/xxx.war 复制到 Tomcat 的 webapps 目录下启动前记得检查配置文件里的数据库连接参数。常见做法是把数据库地址、账号、密码放到 application.yml 或者 jdbc.properties不要写死在代码里。很多源码默认连接 localhost部署到演示环境时改一处配置就行。6.3 交付源码前清一遍检查清单最后一步不是打包而是清理源码里不适合交付的东西。我吃过亏交出去的源码里带着本机绝对路径和测试垃圾数据对方导入后跑不起来来回排查浪费一晚上。检查项具体内容为什么查数据库脚本有独立的 init.sql包含建库和演示数据没有脚本等于黑匣子配置文件数据库地址、账密外置到配置避免改代码才能连库绝对路径全文搜索D:/或/home/换台机器就路径失效源码编码全部文件统一 UTF-8避免编译乱码演示数据至少 3 条航班、2 个用户、1 个订单演示效果直接决定评价测试垃圾删掉本地测试产生的多余订单和日志避免数据脏乱README写清 JDK、MySQL 版本和部署步骤让接手的人能独立复现我从第一次交付源码开始就固定在打包前重新建库跑一遍把每个页面点一次再按清单过一遍。这个习惯帮我把翻车概率降得很低。改签功能的验证尤其要多测几遍因为涉及原订单取消和新订单创建一旦事务边界没写对库存很容易对不上。希望这次拆解能帮你在拿到源码后少走弯路把时间花在真正有价值的业务改造上希望帮到你。本文还有配套的精品资源点击获取
返回列表