简介:面向软件工程课程设计需求,一套基于SSM(Spring+SpringMVC+MyBatis)框架的机票/火车票预定系统完整项目,提供可直接运行并支持二次开发的完整工程代码。项目涵盖用户注册登录、航班/车次查询、在线预订、订单管理等典型模块,涉及前台用户与后台管理员双角色,适合正在完成课设或学习企业级Java开发的学生参考,也是系统学习SSM整合的实用样例。压缩包共1651个文件,以Java源码、JSP动态页面、JS脚本与CSS样式等前端资源为主导,同时包含Spring/MyBatis配置文件、SQL初始化脚本及依赖jar包,并附有png/jpg图片素材,整体大小约50.16MB,资源分类清晰,便于按需查阅。已有273人浏览学习。通过该项目可直观理解SSM的整合方式与分层思想,掌握MyBatis动态SQL、Spring事务管理及SpringMVC请求流转等关键实践,还能借鉴其目录结构和编码规范,快速搭建出符合课程设计要求的预订系统,整体适用于课程设计答辩等场景。
1. 为什么课设票务系统必须先把设计做厚:从一次演示翻车说起
那年某高校不少同学都在做同一个课设:机票火车票预定系统,技术选型清一色 SSM——Spring、SpringMVC、MyBatis 三件套。很多人觉得这题目太常见了,把用户登录、车次列表、下单这三张表一建,增删改查一写,跑起来就算交差。结果答辩现场一演示,问题全来了:两个浏览器同时点「提交订单」,两张订单抢到了同一张票;后台改个车次价格,前端列表半天不刷新;导师问一句“你的订单超时未支付怎么处理”,当场答不上来。这不是代码写得少,是需求没立住就开始写,把软件工程课设定的系统做成了纯 CRUD 练习。这篇方案要解决的就是这个问题:把机票火车票预定系统按软件工程完整流程走一遍,从需求分析、数据库设计、SSM 分层实现到验收答辩,每一步都给可落地的设计决策和代码骨架,适合正在做课程设计的学生参考,也适合想快速上手 SSM 订单类系统的开发者直接改。
2. 需求与可行性分析:划清系统边界,别把课设做成无限膨胀的系统
2.1 角色与核心业务流程:普通用户和管理员两条主线的交织
做系统第一步不是建表,是把“谁在用、怎么用”画清楚。机票/火车票预定系统里只有两类角色:普通用户和管理员。普通用户走完整预订链路——注册登录、搜索车次航班、选择班次下单、支付出票、查看订单、退票改签;管理员走后端管理链路——维护车次航班信息、管理票仓库存、查看处理所有订单、对账统计。两条主线不是孤立的,用户在系统里做的每一次操作,最终都会写进订单表和管理员的操作记录里。
一个合格课设最容易丢分的地方就在业务边界的划定。有些同学把管理员做成全能角色,既能查订单又能给用户重置密码,甚至能直接改数据库里的价格,答辩时被问“权限为什么不分离”就哑口无言。这个系统的角色边界应该是:用户只能操作属于自己的订单,管理员只能管理基础数据和订单状态,且管理员的线下业务不涉及用户资金变动。系统边界可以简单画成这样一个闭环:用户查询余票 → 提交订单 → 模拟支付 → 系统出票 → 用户退票 → 管理员在后台复核订单流水。
2.2 功能需求与非功能需求:余票库存、订单超时这些才是真正的坑
功能需求从业务闭环拆,主要包括五大块:用户模块负责注册、登录、个人信息维护;票务查询模块支持按出发地、目的地、出发日期检索,列车分高铁、动车、普快,航班分经济舱、商务舱;订单模块负责生成订单、锁定票额、模拟支付、出票或退票;后台管理模块负责车次/航班信息维护、余票调整、订单列表查看与订单状态修改;统计模块给管理员看每日订单量和销售额。
非功能需求才是答辩时拉开差距的地方。一是并发正确性,两个人同时抢最后一张票时,系统不能超卖,这必须靠事务和锁解决,而不是在 Java 代码里加个 synchronized 就完事;二是数据一致性,支付失败时订单要回滚到待支付或取消状态,不能让票额凭空减少;三是订单超时机制,用户下单后 15 分钟未支付,系统要自动取消并释放票额。性能和界面反而不是课设首要目标,把业务正确性做扎实,答辩就成功了一大半。
2.3 技术选型说明:为什么是 SSM 而不是 Spring Boot
这可能是导师问得最多的问题。选 SSM 的理由要站在软件工程课程设计的立场说:SSM 是经典的教科书式分层架构,表现层、业务层、持久层边界清晰,能让文档结构和代码结构一一对应,评审老师看设计文档就能对上代码;MyBatis 的 SQL 是手写 SQL,适合展示对数据库连接、事务边界、结果集映射的理解;Spring 的声明式事务适合讲解下单这种强一致性场景的事务控制。相比之下 Spring Boot 启动快、配置少,更适合做小工具,但作为软件工程课设,SSM 恰恰能把“设计”两个字体现出来。
前端技术可以配合 JSP + Bootstrap 或 JSP + LayUI,JSP 是 SSM 课设最常见搭档,因为 SpringMVC 返回 ModelAndView 配上 JSP 的 JSTL 标签,演示时能看到页面跳转和服务端渲染过程。数据库用 MySQL 5.7 就够,用 8.0 也可以,但注意驱动版本要对应,这个问题放到后面避坑章节单独说。
3. 总体设计:三层架构、五张核心表与订单状态机的流转设计
3.1 SSM 分层的模块职责划分:表现层、业务层、持久层各自管什么
总体设计不是画个架构图就算完,更关键的是告诉别人“为什么这样分”。标准的 SSM 工程结构分为四个包:controller(表现层)、service(业务层)、dao(持久层)、pojo/entity(实体类)。表现层只做参数接收和视图转发,业务层写具体逻辑,持久层用 MyBatis 的 Mapper 接口配 XML 文件操作数据库。
模块划分要有明确的宿主位置。用户模块的核心逻辑在 UserService,票务查询在 TicketQueryService,订单模块是关键,OrderService 里要承载事务控制,因为下单涉及订单表插入、车次余票扣减、用户账户状态校验三个数据变更动作。我把订单模块专门拆成 OrderService 和 OrderStateHandler 两个类,前者管业务入口,后者管状态流转,答辩时导师问“一个订单状态在哪些位置被修改”,直接看 OrderStateHandler 就清楚。
这种划分最大的好处是便于文档对照。我在设计文档里写“订单模块采用状态机驱动”,代码里就真的有一个 OrderStateHandler,各状态之间的流转关系直接看方法调用就能理清,不用靠猜。
3.2 数据库设计:用户表、车次航班表、订单表与余票库存表
数据库是整个系统能不能自圆其说的基座。这个系统的核心表一共五张:用户表、车次/航班表、余票库存表、订单表、订单状态日志表。下面是订单表和车次表的核心建表语句,重点是字段类型和约束:
CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL COMMENT 'MD5加密后存储', `real_name` VARCHAR(20), `id_card` VARCHAR(18) COMMENT '乘车人证件号', `phone` VARCHAR(20), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `ticket_stock` ( `id` INT NOT NULL AUTO_INCREMENT, `trip_id` INT NOT NULL COMMENT '关联车次/航班ID', `trip_type` TINYINT NOT NULL COMMENT '1=火车 2=机票', `departure` VARCHAR(50) NOT NULL, `destination` VARCHAR(50) NOT NULL, `depart_date` DATE NOT NULL, `depart_time` TIME NOT NULL, `total_stock` INT NOT NULL DEFAULT 0 COMMENT '总票额', `stock` INT NOT NULL DEFAULT 0 COMMENT '实际余票', `price` DECIMAL(10,2) NOT NULL, `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (`id`), KEY `idx_depart_dest` (`departure`, `destination`, `depart_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='余票库存表'; CREATE TABLE `order_info` ( `id` INT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号,业务生成', `user_id` INT NOT NULL, `trip_id` INT NOT NULL, `trip_type` TINYINT NOT NULL, `passenger_name` VARCHAR(20) NOT NULL, `passenger_id_card` VARCHAR(18) NOT NULL, `ticket_count` INT NOT NULL DEFAULT 1 COMMENT '购票数量', `total_amount` DECIMAL(10,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已出票 2已取消 3已退票', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `pay_time` DATETIME, `cancel_time` DATETIME, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), CONSTRAINT `fk_order_trip` FOREIGN KEY (`trip_id`) REFERENCES `ticket_stock`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这里面有两个细节是课设里最常被忽略的。一是余票库存表和车次信息表为什么要分开,因为车次的车次号、出发到达站是相对静态的数据,而余票是会被并发扣减的动态数据,把库存单独拿出来,锁的粒度就小了,并发性能更好,答辩时可以多解释一句。二是 version 字段,这是乐观锁的核心,下单时先查版本号,更新余票时带上 version 条件,更新的行数为 0 说明别人已经改过,需要重新处理。status 字段用 TINYINT 存状态码,比直接存中文字符串更规范,状态转换规则必须和代码里 OrderStateHandler 保持一致。
3.3 订单状态机设计:待支付、已出票、已取消、已退票四态的流转约束
订单状态是整个系统的灵魂。很多同学做订单模块就是简单地把 status 字段改来改去,用户点取消就置 2,管理员点退票就置 3,完全不考虑状态之间是否允许跳转,结果用户对已退票的订单再次点击取消,系统就崩了。这就是没做状态机设计的问题,我从设计文档的角度建议这样定:订单初始状态是待支付,只有待支付能通向已支付(出票)或已取消;已出票状态只能通向已退票;已取消和已退票是终态,不再接受任何状态变更。超时未支付的服务自动将待支付置为已取消,同时回补库存,这个动作由定时任务触发。
状态日志表的作用是记录每次状态变更的轨迹,字段包含订单号、原状态、新状态、操作类型、操作时间、操作人。这个表看起来简单,答辩时用处非常大——导师问“我怎么验证你的状态规则真的生效了”,你直接把日志表按时间列出来,谁在什么时候把什么状态改成了什么状态,一目了然,比口头解释状态流转有说服力得多。
4. 核心模块实现:余票查询、防超卖下单、支付超时自动取消
4.1 余票查询:MyBatis 动态 SQL 与多条件拼接的设计
查询模块看起来简单,实际是最容易写出烂代码的地方。我见过有人用 String 拼接 SQL,条件多了之后整个方法几百行,字符串拼接还容易引入 SQL 注入。正确做法是 MyBatis 的动态 SQL,用<where>和<if>标签做条件拼接。这里列出 Mapper XML 里的核心查询逻辑:
<select id="queryTicketList" parameterType="map" resultType="com.coursework.pojo.TicketStock"> SELECT ts.id, ts.trip_id, ts.trip_type, ts.departure, ts.destination, ts.depart_date, ts.depart_time, ts.stock, ts.price FROM ticket_stock ts <where> <if test="departure != null and departure != ''"> AND ts.departure = #{departure} </if> <if test="destination != null and destination != ''"> AND ts.destination = #{destination} </if> <if test="departDate != null"> AND ts.depart_date = #{departDate} </if> <if test="tripType != null"> AND ts.trip_type = #{tripType} </if> </where> ORDER BY ts.depart_date, ts.depart_time </select>这段 XML 的价值在于把 SQL 维护的工作从 Java 代码里剥离出来。参数说明:departure 是出发地,destination 是目的地,departDate 是出发日期,tripType 是交通工具类型。当用户不选择目的地时,对应的<if>条件自动跳过,SQL 语句会动态变化,MyBatis 会自动过滤多余的空格和 AND 连接符。这个大括号传参为什么必须用 map?因为多条件组合时用@Param注解逐个传参容易乱,Mapper 接口统一接收 map,XML 里用#{key}取值,清晰可维护。
4.2 防超卖下单:事务边界、行级锁与乐观锁版本号的三重配合
下单是系统里事务性最强的操作,核心流程是:校验余票充足、创建订单记录、扣减余票、返回订单号。最容易翻车的地方是扣减余票这一步,两个并发请求同时读到 stock 剩余 1,都走完校验,最终 update 执行两次,库存变负数,这就是超卖。解决超卖有两条路线,我推荐组合使用。
让代码跑起来,这是 service 层下单核心方法:
@Service public class OrderService { @Autowired private TicketStockMapper stockMapper; @Autowired private OrderMapper orderMapper; @Autowired private OrderStateLogMapper stateLogMapper; /** * 提交订单:创建订单 + 扣减库存 * 注意:这里的事务范围是整个方法,任何一个操作失败都全部回滚 */ @Transactional(rollbackFor = Exception.class) public String createOrder(OrderCreateDTO dto) { // 1. 查询车次余票信息 TicketStock stock = stockMapper.selectByIdForUpdate(dto.getTripId()); if (stock == null) { throw new BusinessException("该车次/航班不存在"); } // 2. 校验余票是否充足 if (stock.getStock() < dto.getTicketCount()) { throw new BusinessException("余票不足,当前剩余 " + stock.getStock() + " 张"); } // 3. 创建订单记录,状态=待支付 OrderInfo order = new OrderInfo(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setTripId(dto.getTripId()); order.setTicketCount(dto.getTicketCount()); order.setTotalAmount(stock.getPrice().multiply(new BigDecimal(dto.getTicketCount()))); order.setStatus(0); orderMapper.insert(order); // 4. 扣减库存,使用 stock = stock - ticketCount int affected = stockMapper.deductStock(dto.getTripId(), dto.getTicketCount()); if (affected == 0) { throw new BusinessException("扣减库存失败,请重试"); } // 5. 记录状态日志:初始状态 stateLogMapper.insert(new OrderStateLog(order.getOrderNo(), -1, 0, "用户提交订单")); return order.getOrderNo(); } }对应 mapper 里selectByIdForUpdate和deductStock的实现:
-- 查询时加行级排他锁,防止两个事务同时读到同一份余票数据 SELECT id, stock, price FROM ticket_stock WHERE id = #{tripId} FOR UPDATE; -- 扣减库存,更新条件带上 stock >= ticketCount,确保不会扣成负数 UPDATE ticket_stock SET stock = stock - #{count} WHERE id = #{tripId} AND stock >= #{count};代码为什么这样写,有几个设计点需要说清楚。select ... FOR UPDATE是关键,两个事务并发时,后到的事务会阻塞在查询语句上,等前一个事务提交或回滚后才继续,这就保证了同一时刻只有一个事务能读到最新余票并去扣减。stock = stock - #{count}和WHERE stock >= #{count}是最后的防线,即使锁没有生效,这条更新语句也会因为条件不满足返回受影响行数 0,从而抛出异常回滚事务。@Transactional(rollbackFor = Exception.class)设定事务回滚条件为所有异常,这一步容易被忽略,默认情况下运行时异常才会回滚,如果方法里抛的是自定义的业务异常,不加rollbackFor会导致事务不生效。generateOrderNo()生成订单号建议格式为时间戳 + 用户 ID + 随机数,保证并发不重复。
4.3 支付模拟与超时取消:定时任务与回补库存的配合
课设一般不接真实支付,用模拟支付按钮代替,点一下就调一个支付接口,把订单状态从待支付改成已出票,记录支付时间。真正有技术含量的是超时未支付自动取消。为什么必须做?因为演示时不可能等 15 分钟让导师看效果,而且系统上线后用户下单不支付会一直占着库存,把票耗光就会产生数据脏账。
这里用 Spring 自带的@Scheduled定时任务实现超时扫描:
@Component public class OrderTimeoutJob { @Autowired private OrderMapper orderMapper; @Autowired private TicketStockMapper stockMapper; /** * 每1分钟扫描一次超时未支付订单 * 超时时间:订单创建时间超过15分钟 */ @Scheduled(cron = "0 0/1 * * * ?") @Transactional(rollbackFor = Exception.class) public void processTimeoutOrders() { // 1. 查出所有待支付且超过15分钟未支付的订单 List<OrderInfo> timeoutOrders = orderMapper.selectTimeoutOrders(15); for (OrderInfo order : timeoutOrders) { // 2. 将订单状态改为已取消 orderMapper.updateStatus(order.getOrderNo(), 2, new Date()); // 3. 回补库存:把订单里的票数加回去 stockMapper.increaseStock(order.getTripId(), order.getTicketCount()); // 4. 记录日志 stateLogMapper.insert(new OrderStateLog(order.getOrderNo(), 0, 2, "订单超时自动取消")); } } }cron 表达式0 0/1 * * * ?意思是从第 0 秒开始,每隔 1 分钟执行一次。selectTimeoutOrders(15)对应 SQL 里的查询条件是status = 0 AND create_time < DATE_SUB(NOW(), INTERVAL #{minutes} MINUTE)。整个定时任务挂事务的原因在于:如果订单状态已经改成取消,但回补库存失败,两个操作不一致会导致余票凭空消失,所以必须保证同生共死。回补库存的 SQL 用SET stock = stock + #{count}即可,不需要加锁条件,因为这是一次性补偿操作。
5. 后台管理、部署配置与常见问题排查:别让课设翻车在细节上
5.1 后台管理模块实现要点:车次管理、订单管理、简易统计报表
后台管理的核心不是花哨的界面,而是数据维护的完整性和操作的可追溯性。车次管理需要支持新增车次/航班、修改价格和日期、调整总票额库存,这个模块的增删改查不复杂,但要做到两点:删除车次时必须校验该车次下是否存在未完成订单,如果存在则禁止删除,只允许停用,我用一个is_active字段控制启用停用,而不是真的删记录;调价和调库存操作要记录操作日志,具体放在订单状态日志表里,操作类型标记为“后台调整”。
订单管理模块要给管理员一个多条件筛选的订单列表,按订单号、用户名、状态、日期范围查询,还要支持管理员直接变更订单状态,比如用户来电要求退票,管理员可以将已出票变更为已退票,同时回补库存。做统计报表不引入 ECharts 这类重型库,用 SQL 聚合查出每日订单数和销售额,展示在一个简单的 JSP 表格里就够了。
后台管理的权限控制用 SpringMVC 的拦截器实现,定义一个LoginInterceptor,在preHandle方法里判断 session 中的用户角色,非管理员访问/admin/**就直接重定向到登录页。这个拦截器实现简单,但答辩时能体现你对 SpringMVC 执行流程的理解。
5.2 部署配置要点:Maven 依赖、Tomcat 运行与 MyBatis 映射
SSM 课设跑不起来十有八九是配置问题,最典型的是 Maven 依赖不一致。Spring 相关依赖版本要统一,我习惯把所有 Spring 依赖的版本固定在一个变量里,比如 5.1.9.RELEASE,Spring 版本和 SpringMVC 版本必须一致,否则会报类冲突异常。MyBatis 配合 Spring 需要引入 mybatis-spring 包,这个包版本也要和 MyBatis 版本匹配。
另外一个高频坑是数据库配置文件jdbc.properties的账号密码没改,连接超时报错。Tomcat 运行 SSM 项目时注意 JDBC 驱动包要打包到 WEB-INF/lib 目录下,如果是通过 Maven 构建,检查是否有<scope>provided</scope>的错误设置,servlet-api 这个依赖必须用 provided,但 mysql 驱动不能用 provided,否则运行时不生效,这个细节导致项目启动成功但一访问就报 ClassNotFoundException,需要重点排查。
5.3 常见问题排查:SSM 项目最容易翻车的五个细节
第一个问题是解决页面中文乱码,现象是页面显示乱码且数据库中文存储也乱码。原因是 JSP 页面编码、SpringMVC 字符过滤器、数据库连接参数、表结构编码四个环节中某一个不一致。解决方式是四层统一为 UTF-8:JSP 顶部加pageEncoding="UTF-8";web.xml 里配置 Spring 的CharacterEncodingFilter强制将请求字符集设为 UTF-8;JDBC URL 加上useUnicode=true&characterEncoding=UTF-8;建表语句统一使用utf8mb4。四层里面漏掉任何一层,乱码都会在某个环节被表现出来,排查时先看数据库存储是否正常。
第二个问题是静态资源报 404,现象是访问 localhost:8080/项目名/css/style.css 时 404。原因是 SpringMVC 的前端控制器 DispatcherServlet 拦截了/路径,把静态资源请求也当成了 controller 映射处理。解决方式是在 SpringMVC 配置文件中加<mvc:default-servlet-handler/>,并配置资源映射<mvc:resources location="/static/" mapping="/static/**"/>。
第三个是事务不生效,现象是下单并发压测时出现超卖,或者插入订单成功但扣减库存失败没有回滚。原因多半是注解扫描漏了 service 包,Spring 容器没有扫描到@Service注解,SpringMVC 的容器又重复扫描导致事务代理失效。解决方式是在 Spring 配置文件的<context:component-scan base-package="com.coursework.service"/>只扫描 service 层,SpringMVC 里只扫描 controller,确保各层各拿各的对象。
第四个是 JSON 日期格式问题,现象是 ajax 请求返回订单列表,日期字段是一串数字时间戳。原因是 SpringMVC 默认用 Jackson 序列化,Date 类型被转成时间戳。解决方式是在实体类的日期字段上标注@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")注解,统一输出字符串格式。
第五个是POST请求 405 错误,现象是页面点提交按钮提示 “Request method POST not supported”。原因通常是 Controller 里方法用了@RequestMapping限制 GET,而页面表单提交方式是 POST。解决方式是先将表单提交方式改成 POST,再在 Controller 方法上用@PostMapping注解。
6. 验收与答辩:用软件工程文档和演示脚本让系统经得起追问
6.1 功能验收对照:从用户注册到退票全链路逐项打钩
到了验收阶段,要对照需求文档里的功能清单逐项验证,不要到了答辩现场才第一次走完整流程。我一般会准备一张验收表,左边是功能模块,中间是验收步骤,右边是预期结果和实际结果。这个系统至少要覆盖以下验收项:新用户注册成功并自动登录;登录失败时有错误提示,密码加密存储无法反查;条件查询能正确过滤车次;下单后订单出现在订单列表且状态为待支付;模拟支付成功后状态变为已出票,库存减一;同时开两个浏览器抢同一趟车的最后一张票时只有一个成功;创建订单后不支付,等 15 分钟订单自动取消且库存回补;管理员后台修改车次价格后,前台查询显示新价格;管理员选中一个订单执行退票操作,订单状态变为已退票,库存回补。
每个验收项都要能一次性通过。我写过一个自查脚本:先注册新用户,再登录查询一条数据,下单,支付,退票,后台改库存,后台查订单,每一步记录截图。答辩演示时按这个脚本走,既不慌乱也不会因为临时操作暴露 bug。
6.2 答辩追问应对:数据库设计、事务、并发三个追问怎么应答
答辩时导师最喜欢追问的集中在这三个方向,史前最惨的一次经历是明明写完了系统却因答不上原理被导师看穿是“背了代码没理解”,从那次以后我强制自己把所有方法都讲清楚才交。
数据库设计被问到的典型问题包括:车次表和余票表为什么拆成两张?答:静态数据与动态数据分离,车次信息不会频繁变化,余票是高频更新的数据,放一起会让锁竞争更严重。订单表为什么需要唯一订单号?答:业务上每个订单需要全局唯一标识,数据库唯一索引兜底并发下重复生成订单号的情况。
事务控制被问到的典型问题包括:什么情况下事务会失效?答:类内部方法自调用时,this.createOrder()调用不会经过事务代理,需要把方法拆到不同类里。rollbackFor为什么必须设置?答:只填@Transactional默认只回滚运行时异常。
并发控制被问到的典型问题包括:FOR UPDATE 锁和乐观锁有什么区别?答:悲观锁读的时候直接锁行,并发性能偏低但实现简单;乐观锁在更新时校验版本号,冲突时重试一次即可,适合高并发读多写少场景。如果导师问项目里为什么两个都用了?答:查库存用悲观锁保证强一致,扣减库存用 update 条件兜底,双保险。
6.3 一个答辩演示技巧:提前准备“失败路径”的演示素材
最后一个技巧是关于演示节奏的。一个容易被忽略的事实是:答辩演示不只是演示功能成功,更要演示异常处理。导师往往更关心你的系统面对异常输入会不会优雅报错,比如输入不存在的车次号下单会提示什么?库存清零之后再下单会不会报错?我准备了两个必演的异常场景:一是余票只剩最后一张,用两个浏览器同时下单,展示只有一个成功,另一个提示“余票不足”,然后切到订单日志看状态记录;二是对一个已取消的订单再次点击支付,系统提示“当前订单不可支付”,不会修改任何数据。这两个场景演示完后,导师基本就确认你是真的写过这套系统,而不只是跑通了 demo。
那次翻车之后我每个课设都强制自己先写验收脚本再做系统,答辩时照着脚本一步步走,等导师追问的时候再进数据库查表辅助说明,整个过程不会手忙脚乱。希望这篇方案能帮你的票务预定课设少踩几个坑,顺利通过验收。
本文还有配套的精品资源,点击获取