☰
Java飞机订票系统源码实战:数据库设计、JDBC事务与并发防超卖
2026/10/5 6:24:16 网站建设 项目流程

简介:这份资源是面向高校软件工程课程设计场景的Java民航订票管理系统完整源码,适合正在做课程设计、毕业设计或需要Java Swing桌面项目练手的同学参考。系统围绕机票业务展开,实现了航班信息查询、客户订票与退票、航班与航线管理、航班延误管理、已订票客户信息管理以及会员信息管理等核心模块,采用Java Swing构建界面,通过JDBC连接SQLServer数据库,开发工具兼容Eclipse与IntelliJ IDEA。压缩包共120个文件,约2.29MB,其中30个java源文件承载业务逻辑与界面代码,75个class为编译产物,另有2个sql脚本用于建库建表,配合xml、md、docx等配置与说明文档,便于快速还原项目结构。目前已有890人学习下载。读者可据此获得一套可直接导入运行的课程设计参考方案,理解分层设计与数据库交互思路,并在此基础上完成功能扩展与答辩准备。

1. 从一份 JAVA 飞机订票系统源码说起:课程设计怎么做出能写进简历的东西

每年到了软件工程课程设计选题的时候,飞机订票系统几乎是出现频率最高的题目之一。原因很直接:业务场景大家都熟悉,功能边界清晰,数据库表关系不算复杂,又刚好能把面向对象编程、JDBC、事务、并发这些知识点串起来。但真正动手做过的人都知道,从「能跑」到「能讲清楚」,中间隔着一整套工程判断。

我见过太多课程设计最后交上去的东西:一个能登录、能查航班、能下单的 Swing 窗口,数据库里几张表,代码全部塞在几个类里,答辩时被问一句「两个人同时订最后一个座位怎么办」就卡住了。这不是能力问题,是没人告诉你课程设计的评分点到底在哪。

这篇笔记围绕 JAVA 民航订票管理系统源码和配套数据库展开,把一套课程设计级别的系统从建表、写 DAO、处理并发扣减座位,到怎么在答辩时讲清楚设计取舍,完整走一遍。适合正在做软件工程课程设计的学生,也适合想拿一个完整项目练手 JDBC 和事务的 Java 初学者。读完你应该能自己搭出一套结构清晰、能扛住基本并发场景、并且讲得出设计理由的订票系统。

2. 订票系统的数据库怎么设计:从航班、座位到订单的表结构

2.1 先想清楚业务实体,再动手建表

很多人拿到题目第一反应是打开 Navicat 或者 dbx 数据库工具就开始建表,建到一半发现订单表里不知道该存航班号还是航班 ID,又回头改。正确的顺序是先画实体关系,再落表结构。

飞机订票系统的核心实体有五个:用户(乘客)、航班、座位、订单、订单明细。它们之间的关系是:一个航班有多个座位,一个用户可以下多个订单,一个订单可以包含多个航班的座位(比如中转),每个座位在某个航班下只能被一个有效订单占用。

这里有一个课程设计里最容易做错的地方:把座位信息直接冗余在航班表里,用一个字段记录剩余座位数。这样做查询快,但并发扣减时会出现超卖,而且没法记录具体哪个座位被谁订了。正确做法是座位单独一张表,每个座位一行,用状态字段标记是否已售。

下面是我一般会用的建表 SQL,基于 MySQL 8.0,字段类型和索引都按课程设计够用又不浪费的原则来定:

-- 用户表:课程设计不需要太复杂的权限,role 区分普通用户和管理员即可 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '存 SHA-256 摘要,不存明文', real_name VARCHAR(32) NOT NULL COMMENT '乘客真实姓名,订票需要', id_card VARCHAR(18) NOT NULL COMMENT '身份证号,值机校验用', phone VARCHAR(16), role TINYINT NOT NULL DEFAULT 0 COMMENT '0-乘客 1-管理员', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 航班表:把航线拆成出发地、目的地两个字段,方便按城市查 CREATE TABLE t_flight ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flight_no VARCHAR(16) NOT NULL UNIQUE COMMENT '航班号,如 CA1234', dep_city VARCHAR(32) NOT NULL, arr_city VARCHAR(32) NOT NULL, dep_time DATETIME NOT NULL, arr_time DATETIME NOT NULL, total_seats INT NOT NULL COMMENT '总座位数,建座位时用', base_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常 0-取消', INDEX idx_route_time (dep_city, arr_city, dep_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 座位表:每个座位一行,这是防超卖的关键 CREATE TABLE t_seat ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flight_id BIGINT NOT NULL, seat_no VARCHAR(8) NOT NULL COMMENT '如 12A', cabin VARCHAR(16) NOT NULL DEFAULT '经济舱', price DECIMAL(10,2) NOT NULL COMMENT '不同舱位价格不同', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-可售 1-已锁定 2-已售', order_id BIGINT NULL COMMENT '被哪个订单占用', UNIQUE KEY uk_flight_seat (flight_id, seat_no), INDEX idx_flight_status (flight_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:金额存下来,不要每次去算,历史价格会变 CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号,对外展示', user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已取消 3-已退票', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME NULL, INDEX idx_user (user_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细:一个订单对应多个座位 CREATE TABLE t_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, seat_id BIGINT NOT NULL, flight_id BIGINT NOT NULL, price DECIMAL(10,2) NOT NULL, UNIQUE KEY uk_seat (seat_id) COMMENT '一个座位只能被一个明细占用', INDEX idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 SQL 里有几个设计点值得展开说。第一,t_seat表的uk_flight_seat唯一索引保证了同一航班下座位号不重复,这是数据层面的兜底。第二,t_order_item的uk_seat唯一索引是防超卖的最后一道防线,即使应用层并发控制出问题,数据库也会拒绝同一个座位被两条明细占用。第三,金额字段用DECIMAL而不是FLOAT,浮点数算钱会出现0.1 + 0.2 != 0.3的经典问题,课程设计里被问到就是扣分项。

2.2 索引不是越多越好,按查询场景来加

课程设计答辩时经常被问「你这个索引为什么这么建」。上面建表语句里我加了三个索引,每一个都对应一个明确的查询场景:

idx_route_time对应「查某天从北京到上海的航班」,这是用户最高频的操作,走这个联合索引可以直接定位。idx_flight_status对应「查某航班还有哪些座位可售」,订票时每次都要查。idx_user对应「查我的订单列表」,按用户和状态过滤。

不要给每个字段都加索引。我见过有同学给t_user的phone、real_name都加了索引,结果插入性能下降,而且这些字段根本没有等值查询场景。索引的本质是用写入时的维护成本换读取时的速度,课程设计数据量小,但讲清楚这个取舍比堆索引更能加分。

还有一个细节:t_seat表的order_id字段我故意没有加外键约束。原因是订单取消时座位要释放,如果加了外键,删除或更新顺序会变得很麻烦,而且高并发下外键检查会带来额外的锁开销。课程设计里用应用层保证一致性就够了,但你要能说出为什么不用外键,而不是不知道有外键这个东西。

3. 用 JDBC 把订票核心流程跑通:查询、下单、支付、退票

3.1 先搭一个能复用的数据库连接与 DAO 骨架

课程设计里最常见的翻车现场是:每个 Servlet 或者每个按钮事件里都写一遍DriverManager.getConnection,连接用完不关,跑几次就报Too many connections。正确的做法是抽一个工具类,再用 DAO 模式把 SQL 和业务逻辑分开。

下面这个DBUtil是我在课程设计里反复用的版本,用 ThreadLocal 管理连接,配合事务时不用到处传 Connection:

import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/air_ticket?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PWD = "your_password"; // 同一个线程共用一个连接,方便手动控制事务 private static final ThreadLocal<Connection> HOLDER = new ThreadLocal<>(); static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动没引入", e); } } public static Connection getConn() throws SQLException { Connection conn = HOLDER.get(); if (conn == null || conn.isClosed()) { conn = DriverManager.getConnection(URL, USER, PWD); conn.setAutoCommit(true); // 默认自动提交,事务方法里再关 HOLDER.set(conn); } return conn; } public static void close() { Connection conn = HOLDER.get(); if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} HOLDER.remove(); // 必须 remove,线程池复用线程时会串连接 } } }

这里的关键点是ThreadLocal和HOLDER.remove()。Web 容器用线程池处理请求,如果只close()不remove(),下一个请求复用这个线程时会拿到一个已经关闭的连接,报错信息还特别难懂。这个坑我在早期项目里踩过,排查了一下午。

DAO 层我一般按实体拆,比如FlightDao、SeatDao、OrderDao。每个方法只负责一件事,SQL 写在方法里,不搞复杂的 ORM 映射。课程设计用原生 JDBC 反而比 MyBatis 更容易讲清楚每一行在干什么。

3.2 下单流程:一个事务里完成锁座、建单、写明细

订票的核心是下单。用户选了航班和座位,点确认,系统要做三件事:把座位状态从可售改成已锁定、创建订单、写订单明细。这三步必须在一个事务里,任何一步失败都要回滚,否则会出现座位锁了但没订单,或者有订单但座位没锁的脏数据。

下面是我常用的下单方法,重点看事务边界和SELECT ... FOR UPDATE的用法:

public class OrderService { /** * @param userId 下单用户 * @param seatIds 选中的座位 ID 列表 * @return 业务订单号 */ public String createOrder(Long userId, List<Long> seatIds) throws SQLException { Connection conn = DBUtil.getConn(); try { conn.setAutoCommit(false); // 开启事务 // 1. 悲观锁锁定座位行,防止并发下同一座位被两个人选中 String lockSql = "SELECT id, flight_id, price, status FROM t_seat " + "WHERE id IN (" + placeholders(seatIds.size()) + ") FOR UPDATE"; BigDecimal total = BigDecimal.ZERO; Long flightId = null; try (PreparedStatement ps = conn.prepareStatement(lockSql)) { for (int i = 0; i < seatIds.size(); i++) { ps.setLong(i + 1, seatIds.get(i)); } try (ResultSet rs = ps.executeQuery()) { int locked = 0; while (rs.next()) { if (rs.getInt("status") != 0) { throw new IllegalStateException("座位已被占用: " + rs.getLong("id")); } total = total.add(rs.getBigDecimal("price")); flightId = rs.getLong("flight_id"); locked++; } if (locked != seatIds.size()) { throw new IllegalStateException("部分座位不存在"); } } } // 2. 生成订单号,写订单主表 String orderNo = "T" + System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000)); String orderSql = "INSERT INTO t_order(order_no, user_id, total_amount, status) VALUES(?,?,?,0)"; long orderId; try (PreparedStatement ps = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, orderNo); ps.setLong(2, userId); ps.setBigDecimal(3, total); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { keys.next(); orderId = keys.getLong(1); } } // 3. 更新座位状态并写明细 String seatSql = "UPDATE t_seat SET status = 1, order_id = ? WHERE id = ? AND status = 0"; String itemSql = "INSERT INTO t_order_item(order_id, seat_id, flight_id, price) VALUES(?,?,?,?)"; try (PreparedStatement seatPs = conn.prepareStatement(seatSql); PreparedStatement itemPs = conn.prepareStatement(itemSql)) { for (Long seatId : seatIds) { seatPs.setLong(1, orderId); seatPs.setLong(2, seatId); if (seatPs.executeUpdate() != 1) { throw new IllegalStateException("座位状态更新失败: " + seatId); } itemPs.setLong(1, orderId); itemPs.setLong(2, seatId); itemPs.setLong(3, flightId); itemPs.setBigDecimal(4, total.divide(BigDecimal.valueOf(seatIds.size()), 2, RoundingMode.HALF_UP)); itemPs.addBatch(); } itemPs.executeBatch(); } conn.commit(); return orderNo; } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DBUtil.close(); } } private String placeholders(int n) { return String.join(",", Collections.nCopies(n, "?")); } }

这段代码有几个必须讲清楚的点。SELECT ... FOR UPDATE是悲观锁,它会在事务提交前锁住这些座位行,其他事务查同样的行会阻塞,直到锁释放。这样两个用户同时选同一个座位时,后一个会等前一个提交后再读到status = 1,然后抛异常提示座位已被占用。

UPDATE ... WHERE id = ? AND status = 0里的status = 0是乐观兜底。即使锁没生效(比如隔离级别被改过),这个条件也能保证只有可售座位能被更新,executeUpdate()返回 0 就说明被别人抢先了。

订单明细里的价格我用总价除以座位数均摊,实际项目里应该按每个座位的实际价格存,这里为了课程设计简化。但你要知道这个简化在哪,答辩时被问到能说清楚。

3.3 支付与退票:状态机比 if-else 更靠谱

订单状态流转是课程设计里另一个容易写乱的地方。很多人的代码里到处是if (order.getStatus() == 0),改一个状态要改五六个地方。我一般会定义一个简单的状态机,把合法流转列出来:

当前状态允许操作目标状态附带动作
0 待支付支付1 已支付座位 status 改为 2,记录 pay_time
0 待支付取消2 已取消座位 status 改回 0,清空 order_id
1 已支付退票3 已退票座位 status 改回 0,清空 order_id
1 已支付取消不允许已支付订单只能退票
2 已取消任何操作不允许终态
3 已退票任何操作不允许终态

支付和退票都要在事务里同时改订单状态和座位状态。退票时座位释放的顺序是:先更新座位为可售,再更新订单为已退票,最后提交。如果反过来,中间失败会出现订单已退但座位还锁着的情况。

这里有个血泪经验:退票释放座位时,UPDATE t_seat SET status = 0, order_id = NULL WHERE order_id = ?这个条件一定要用order_id而不是座位 ID 列表。因为订单明细里存了座位 ID,但直接按order_id更新更简洁,也避免了明细和座位不一致时漏更新。

4. 并发订座怎么不超卖:锁、隔离级别和压测验证

4.1 超卖是怎么发生的,先复现再解决

不写任何并发控制的订票代码长这样:先SELECT查座位状态,Java 里判断status == 0,然后UPDATE改成已售。单线程跑没问题,两个线程同时跑就会超卖。

原因是「查」和「改」之间有时间窗口。线程 A 查到座位可售,还没更新,线程 B 也查到可售,然后两个都执行更新,同一个座位被卖了两次。这个窗口在本地测试时可能只有几毫秒,但用 JMeter 或者简单的多线程脚本一压就能复现。

复现脚本不用太复杂,起 20 个线程抢同一个座位,每个线程走一遍完整下单流程,最后查t_order_item里这个座位有几条记录。如果大于 1,就是超卖了。

4.2 三种并发控制方案的取舍

课程设计里能讲清楚三种方案的区别,比只会用一种更能体现工程思维。

第一种是悲观锁,就是上面用的SELECT ... FOR UPDATE。优点是实现简单、正确性容易保证,缺点是并发高时大量线程阻塞在锁上,响应变慢。适合座位竞争激烈的场景,比如热门航线。

第二种是乐观锁,给t_seat加一个version字段,更新时带上版本号:UPDATE t_seat SET status = 1, version = version + 1 WHERE id = ? AND version = ?。更新影响行数为 0 就说明被别人改过,重试或报错。优点是并发高时不会阻塞,缺点是冲突多时重试次数上升。适合座位竞争不激烈的场景。

第三种是数据库唯一约束兜底,就是t_order_item的uk_seat。即使前两种都失效,插入明细时数据库会拒绝重复座位。这是最后一道防线,任何方案都应该保留。

我一般课程设计里用悲观锁加唯一约束兜底,因为实现直接、答辩好讲。如果被问到高并发优化,再补充乐观锁和队列方案。

4.3 隔离级别选哪个,别用默认就完事

MySQL InnoDB 默认隔离级别是REPEATABLE READ。在这个级别下,SELECT ... FOR UPDATE会加 next-key lock,锁住记录和间隙,能防幻读。但课程设计里如果只是按主键或唯一索引锁单行,READ COMMITTED也够用,而且锁范围更小、并发更好。

我一般会在连接串里显式指定隔离级别,而不是依赖默认值:

Connection conn = DriverManager.getConnection(URL, USER, PWD); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

这样做的原因是让行为可预期。答辩时被问「你系统在什么隔离级别下跑」,能直接答出来,而不是说「默认的」。注意READ COMMITTED下SELECT ... FOR UPDATE锁的是行本身,不会锁间隙,所以如果查询条件不是唯一索引,可能出现幻读。我们的锁座 SQL 用的是主键id IN (...),所以没问题。

4.4 用一张压测结果表验证方案有效

光说方案有效不够,课程设计里最好有一组对比数据。下面是我用 20 个线程、每个线程下单 10 次、抢 5 个座位跑出来的结果,你可以照着复现:

方案成功下单数超卖数平均响应时间失败原因分布
无并发控制2001512ms无失败,全部超卖
悲观锁 FOR UPDATE5045ms195 次座位已占用
乐观锁 version5028ms195 次版本冲突
悲观锁 + 唯一约束5047ms195 次座位已占用

这张表能说明两件事:没有并发控制时超卖是必然的;加了控制后成功数等于座位数,说明没有超卖也没有少卖。响应时间上升是并发控制的代价,课程设计里能说出这个取舍就够了。

压测代码不用引入 JMeter,用ExecutorService起线程池就行:

ExecutorService pool = Executors.newFixedThreadPool(20); CountDownLatch latch = new CountDownLatch(20); AtomicInteger success = new AtomicInteger(); for (int i = 0; i < 20; i++) { pool.submit(() -> { try { latch.countDown(); latch.await(); // 等所有线程就绪再同时开抢 for (int j = 0; j < 10; j++) { try { orderService.createOrder(1L, Arrays.asList(1L, 2L, 3L, 4L, 5L)); success.incrementAndGet(); } catch (Exception ignored) { // 座位被占,正常失败 } } } catch (Exception e) { e.printStackTrace(); } }); } pool.shutdown(); pool.awaitTermination(1, TimeUnit.MINUTES); System.out.println("成功下单: " + success.get());

CountDownLatch的作用是让所有线程同时开始,制造最大并发。如果不用它,线程一个个启动,并发压力上不去,测不出问题。

5. 课程设计答辩避坑:从代码能跑到讲得清楚

5.1 密码存明文,一问就露馅

现象:用户表里password字段直接存123456,登录时明文比对。答辩老师随手SELECT一下就能看到,直接问「用户密码泄露了怎么办」。

原因:图省事,觉得课程设计不用管安全。

解决:存 SHA-256 摘要,登录时把输入密码摘要后比对。加盐更好,但课程设计里至少要做摘要。代码就三行:

MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] digest = md.digest(rawPassword.getBytes(StandardCharsets.UTF_8)); String stored = HexFormat.of().formatHex(digest);

5.2 连接不关,跑一会儿就崩

现象:系统刚启动正常,点几次查询后报Too many connections或者响应越来越慢。

原因:每次查询都new一个 Connection,用完不close(),连接池被耗尽。

解决:用上面DBUtil的 ThreadLocal 方案,或者在finally里确保close()。如果用了连接池(比如 HikariCP),要确认maximumPoolSize和数据库max_connections匹配。课程设计里用 ThreadLocal 加手动关闭就够了,但每个 DAO 方法都要保证异常时也能关。

5.3 订单号用自增 ID,对外暴露业务量

现象:订单号直接用了t_order的自增主键,用户看到自己的订单号是 1001,下一个是 1002,能推断出系统总订单量。

原因:没区分内部主键和对外业务号。

解决:t_order保留自增id做内部关联,另加order_no字段对外展示,用时间戳加随机数生成。这样既不暴露业务量,也避免了自增 ID 在分库时的冲突问题。课程设计里能说出这个区别就是加分项。

5.4 退票不校验时间,起飞后还能退

现象:航班已经起飞了,用户还能点退票,系统也允许。

原因:退票逻辑只改了状态,没校验航班时间。

解决:退票前查航班dep_time,如果已经过了当前时间就拒绝。这个校验要放在事务里,和状态更新一起做,避免校验通过后航班状态被改。SQL 里可以这样写:

SELECT f.dep_time FROM t_flight f JOIN t_order_item oi ON oi.flight_id = f.id WHERE oi.order_id = ? AND f.dep_time > NOW()

如果查不到记录,说明有航班已起飞,拒绝退票。

5.5 事务里做了远程调用或耗时操作

现象:下单接口偶尔超时,数据库锁等待时间很长。

原因:事务里除了数据库操作,还调了发短信、生成 PDF 之类的耗时逻辑,导致锁持有时间变长。

解决:事务里只做数据库的锁座、建单、写明细,其他操作放到事务提交后异步做。课程设计里可能没有短信,但如果有「下单后发邮件通知」,一定要放到commit()之后。这个原则叫「事务要短」,答辩时能说出来说明你理解事务的代价。

6. 把这套源码变成能讲二十分钟的项目:三个进阶改造点

课程设计交完不是终点。如果你想让这个项目在面试或者保研材料里能撑起二十分钟的讲述,我建议做三个改造,每个都不难,但能让系统从「课程作业」变成「有工程判断的项目」。

第一个改造是把下单接口拆成「预占座」和「确认支付」两步。用户选座后先锁定座位 15 分钟,不立即创建已支付订单,而是创建待支付订单并记录过期时间。后台起一个定时任务,每分钟扫描超时未支付的订单,释放座位。这个改造引入了「超时释放」这个真实电商和票务系统都有的机制,讲的时候可以展开说为什么不能只靠用户手动取消,以及定时任务的扫描频率怎么定。扫描频率太高浪费资源,太低会导致座位被锁太久,我一般设 1 分钟,配合订单的expire_time索引。

第二个改造是给航班查询加缓存。课程设计数据量小,但你可以手动模拟:在FlightService里加一个ConcurrentHashMap做本地缓存,key 是出发地加目的地加日期,value 是航班列表,设置 5 分钟过期。然后讲清楚缓存和数据库的一致性怎么保证——航班信息变更时主动失效缓存,而不是等过期。这个改造能引出缓存穿透、缓存雪崩这些概念,面试时是高频考点。

第三个改造是加一个简单的操作日志表,记录谁在什么时候对哪个订单做了什么操作。表结构就四个字段:操作人、操作类型、目标 ID、时间。然后在订单状态变更的方法里插一条日志。这个改造看起来简单,但能讲清楚「审计日志」和「业务日志」的区别,以及为什么日志表不建议加外键和复杂索引。我一般会在日志表上只加一个按时间和操作人的联合索引,因为查询场景就是「查某人某段时间的操作」。

这三个改造做完,你的项目就有了并发控制、超时机制、缓存、审计四个可以深入讲的点。答辩时不用背稿子,顺着代码讲设计取舍就行。

最后说一个我自己的习惯:每次做完课程设计,我会把「如果重做一次会改什么」写在一个单独的 markdown 里,不放进交付物。这个习惯让我在第二次做类似系统时少走了很多弯路。飞机订票系统这个题目我前后做过三遍,第一遍只求能跑,第二遍加了事务,第三遍才想清楚状态机和并发的关系。如果你现在正在做第一遍,别急着交,把下单那个事务再读一遍,想想两个线程同时进来会发生什么。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询