航空订票系统UML建模:用例到类图时序图闭环

发布时间:2026/11/2 17:37:31
航空订票系统UML建模:用例到类图时序图闭环

简介:《航空订票系统 UML 建模设计》文档面向软件工程课程设计、UML建模练习及系统分析入门者,围绕航空订票场景给出从需求到动态建模的设计参考。资源包共1个docx文件,约535KB,内容以需求分析、用例图、类图、状态图、顺序图、合作图组织,覆盖订票、航班信息管理、个人信息管理三个核心模块。文档将用户、管理员与顶层用例分别展开,并梳理用户类、管理员类、航班信息类、机票类、个人信息类之间的关系;状态图关注登录、查询、预订、退票等状态变化,顺序图与合作图呈现注册、登录、查询、预订、退票的交互过程,同时给出包含基本事件流、异常事件流等要素的用例规约。已有1627人学习,适合课程作业对照搭建建模框架、补充图形表达,也可作为系统设计文档的参考底稿。

1. 从“图画完了却讲不清”说起

航空订票系统 UML 建模设计最容易出问题的地方,往往不是图本身画得丑,而是图画完之后、代码要开工时,发现用例和类根本对不上。我见过一个课程设计小组,三个人花两周画了十几张图,答辩时被问“退票时座位怎么回滚”,全场沉默——因为他们的类图里压根没有“座位余量”这个概念,时序图也只画到“点击退票按钮”就结束了。这套方案要解决的就是这件事:把一份订票子系统的建模,从需求边界一路推到可以交付、可以自证的图集。它适合要交毕业设计或课程设计的学生,也适合需要给团队留一份设计底稿的从业者。核心不是认全 UML 符号,而是让 16 条用例、5 个核心类、4 张时序图彼此能对应上,形成一条完整的证据链。

2. 需求边界:三类参与者与 16 条用例怎么划才干净

2.1 参与者识别:游客、注册用户、管理员的三层权限

参与者划错,后面所有图都会跟着偏。这个订票子系统把浏览者拆成“游客”和“注册用户”两类,看起来只是多画一个小人,实际决定了用例边界:游客只能到首页,能触发的用例只有“注册”和“登录”两个,剩下的全部挂在注册用户名下。很多方案在这里图省事,只画一个“用户”参与者,然后把“登录”写成所有用例的前置条件——这样做的代价是,你没法解释“未登录状态下点击订票按钮”时系统该返回什么。

管理员这条线更需要单独拆。它不是普通的注册用户升级而来,而是由航空公司内部员工号或最高权限管理员派发的标识符来区分,这属于典型的特殊注册流程。建模时要把它体现在两处:一是参与者的泛化关系,管理员在“登录、查看个人信息、修改个人信息”这三件事上和注册用户行为一致,可以抽出一个“系统用户”父类参与者;二是敏感操作的二次验证,“添加航班信息”“修改航班信息”这类用例的前置条件里要明确写“已通过标识符二次校验”,不能只写“管理员登录后”。

我一般会在需求阶段就把权限矩阵画成一张小表,逼着自己回答“谁能做什么”。这张表不写进最终图集也没关系,但它能帮你挡住后面 80% 的返工。

功能游客注册用户管理员
访问首页允许允许允许
注册、登录允许允许允许(凭标识符)
航班查询禁止允许允许
机票预订、付款、退票禁止允许禁止
航班信息增删改禁止禁止允许(需二次验证)
统计订票数据禁止禁止允许
打印票据禁止允许禁止

2.2 用例清单:16 条用例的编号、参与者与前置条件

原始需求里一共整理了 16 条用例,编号从 1 到 16。这个编号体系要保留下来,因为后面类图、时序图、测试用例都要引用它,编号一乱,整个图集就没法交叉核对。清单大致是这样一个结构:

用例 ID用例名称参与者前置条件
1用户注册未注册用户进入注册页面
2用户登录注册用户、管理员进入登录界面
3航班查询注册用户、管理员登录成功并进入查询页面
4机票预订注册用户进入查询结果页面
5已订机票查询注册用户进入查询页面
6退票注册用户进入查询页面,航班未发出
7修改已订航班个人信息注册用户进入修改页面
8查询个人信息注册用户、管理员进入查询页面
9修改个人信息注册用户、管理员已查询到个人信息
10打印票据注册用户已预订机票
11添加航班信息管理员进入添加页面并通过二次验证
12修改航班信息管理员进入修改页面并通过二次验证
13删除航班信息管理员进入航班查询页面
14统计订票数据管理员进入统计页面
15订票管理注册用户进入相关页面
16航班信息管理管理员具备相应权限

这张表里有两处需要特别留意。用例 3 的参与者写的是“注册用户、管理员”,但管理员用例图里并没有列出“航班查询”这一项,这是一个典型的不一致,后面第 5 章会专门讲怎么处理。用例 15、16 是打包性质的用例,它们和 4、5、6 以及 11、12、13 是包含关系,画图时用<include>指过去就行,不用重复展开——但前提是你在规约里写清楚,否则评阅人会认为你漏了细节。

2.3 用例规约:把基本事件流写成可测试的步骤

用例图只回答“有什么功能”,用例规约回答“怎么算做完了”。很多方案把规约写成一段小说式的叙述,读完还是不知道异常怎么处理。我推荐固定的四段式:参与者动作、系统响应、异常事件流、后置条件,其中异常流必须逐条列出提示文案。

以用例 4“机票预订”为例,基本事件流只有两步:用户点击预订按钮;系统取机票信息和用户信息,生成订单并落库。异常流则要想得细,比如“座位已满”时提示“该航班余票不足”,“航班状态已截载”时提示“该航班已停止售票”。后置条件写“进入订票成功页面,订单状态为待支付”。

再以用例 6“退票”为例,它的核心约束藏在业务规则里:只有在航班未发出时才能退。这条规则落到规约里要写成明确的前置条件和异常流,落到类图里要变成Flight.isRefundable()这样的方法,落到时序图里要变成一次“查航班状态”的消息。同一件事在三张图里各出现一次,才叫建模闭环。

提示:异常事件流不要写“系统提示错误信息”这种笼统的话。写成具体文案(如“用户未订任何机票”“两次密码不同”)之后,后面写测试用例可以直接照抄,答辩时也能拿出来当证据。

2.4 三张用例图的分层:顶层图不画系统内部

这个系统的用例图分成顶层用例图、用户用例图、管理员用例图三张,这个分层方式是合理的,因为参与者的权限差异太大,塞进一张图会变成蜘蛛网。

顶层用例图只放三个用例:个人信息管理、订票管理、航班信息管理,参与者是注册用户和管理员。它的作用是给评阅人一个鸟瞰视角,所以不要在顶层图里出现“打印票据”“统计订票数据”这种细节,那是展开图的工作。

用户用例图包含 10 条用例,管理员用例图包含 8 条。两张图有 3 条重叠(注册、登录、查询/修改个人信息),处理方式有两个:一是用参与者泛化,画一个“系统用户”父类,把公共用例挂上去,两张子图只画各自特有的部分;二是在两张图里重复画,接受少量冗余来换取可读性。课程设计场景下我更倾向第二种,因为泛化关系画多了容易让图变得抽象,反而不好讲。从业者做正式交付件时用第一种,配一份参与者—用例对照表,维护成本更低。

3. 静态结构建模:类图里到底该放什么

3.1 从用例规约里抽类:名词筛选加职责校验

类不是拍脑袋想出来的,是从用例规约里“筛”出来的。具体做法是:把 16 条规约通读一遍,把所有名词圈出来,得到一份候选清单——用户、管理员、航班、机票、订单、个人信息、座位、付款记录、票据、查询条件。然后逐个过三道筛子:它是不是系统要持久保存的东西?它有没有自己的行为?它是不是只是别的类的属性?

按这三条筛,“查询条件”被淘汰,它是方法的入参;“票据”被合并进订单,因为打印只是订单的一个状态标记而不是独立实体;“座位”在这个简化系统里退化成航班的两个整数字段(总座位数、已售数),不单独建类——如果你的系统要做选座,那座位必须独立成类,这是建模粒度的取舍,取决于需求,不取决于图好不好看。

筛完剩下 5 个核心类:用户类、管理员类、航班信息类、机票类、个人信息类。这里有个争论点:个人信息该不该独立成类?如果它只是姓名、证件号、联系方式,那它更像用户类的一组属性。但它之所以被单列,是因为“修改个人信息”和“修改已订航班个人信息”是两条不同的用例——后者改的是订单上的旅客信息,属于下单时的快照,不能随用户资料变更而改变。所以务实的做法是:个人信息独立成类,与用户一对一存当前资料,在机票类里再存一份快照。

3.2 五个核心类的属性与方法

类图画到什么粒度算够?我的判断标准是:每个类的方法要能在时序图里找到对应的消息。找不到对应消息的方法就是臆想出来的,删掉;时序图里有消息但类上没有方法,就是漏了,补上。

// 用户类:注册用户与管理员共用身份字段,权限差异由 role 区分 public class User { private Long userId; // 用户唯一标识,订单表以外键引用 private String userName; // 登录名,注册时做非空校验 private String password; // 密文存储,注册时校验"两次输入密码不同" private Role role; // REGISTERED / ADMIN,对应两类参与者 private Profile profile; // 一对一关联,个人信息归属端 // 登录:成功返回会话令牌,失败由调用方按规约给出具体提示 public Session login(String userName, String password) { ... } // 注册:内部校验用户名重复,对应用例 1 的异常流 public boolean register(Profile profile) { ... } } // 航班类:承载"航班未发出才可退票"这条业务规则的判断 public class Flight { private String flightNo; // 航班号,业务主键 private String fromCity; // 出发地 private String toCity; // 目的地 private LocalDateTime departTime;// 起飞时间,与当前时间比较得出是否可退 private int seatTotal; // 总座位数 private int seatSold; // 已售数,退票时递减 private FlightStatus status; // SCHEDULED/OPEN/CLOSED/DEPARTED/CANCELLED public boolean isRefundable() { return status == FlightStatus.OPEN || status == FlightStatus.SCHEDULED; } public int remainingSeats() { return seatTotal - seatSold; } }

机票类(也可以叫订单类)是整个模型的重心,它的状态字段直接决定了状态图怎么画:

// 机票类:一次预订产生一条记录,状态流转见第 4 章的状态图 public class Ticket { private Long ticketId; private Long userId; private String flightNo; private Profile passengerSnapshot; // 下单瞬间的旅客信息快照 private BigDecimal amount; private TicketStatus status; // UNPAID/PAID/ISSUED/REFUNDED/USED private boolean printed; // 打印票据用例的幂等标记 public boolean pay(BigDecimal amount) { ... } // 付款,仅 UNPAID 可调用 public boolean refund() { ... } // 退票,需航班可退 public String printTicket() { ... } // 打印,需 status=ISSUED }

参数说明上有个细节值得讲:departTime用了LocalDateTime而不是字符串,是为了让“航班未发出”这条规则可以直接在代码里比较;status用枚举而不是整型,是为了让状态图上的每个状态在类图里都能找到名字。这类选择在答辩时经常被追问,提前想好理由比事后补强得多。

3.3 类间关系与多重性:关联、聚合、依赖的取舍

关系画错比类画少更致命。这五个类之间的关系大致是:

  • 用户与个人信息:一对一关联,个人信息是用户的一部分,用组合(实心菱形)更贴切,因为用户注销时资料应随之失效。
  • 用户与机票:一对多关联,一个用户可有多条订单,多重性写1 .. 0..*。
  • 航班与机票:一对多关联,一个航班对应多条订单,机票端持有flightNo外键。
  • 管理员与航班:一对多关联,表示管理职责,多重性1 .. 0..*。
  • 机票与个人信息快照:这是一种依赖关系,订单生成时拷贝一份,之后不再随源变化。

聚合和组合在这里最容易画反。我的经验是问一句“整体没了,部分还活不活”:用户注销后个人信息没有独立存在的意义,用组合;航班取消后历史订单仍然要保留可查,用普通关联而不是组合,否则评阅人会问“航班删了订单跟着删吗”,那就成了事故。

3.4 类图落到表结构:让图和库对得上

类图画完最好顺手落一版建表语句,这是自证一致性的最快方式——如果某个类落不成表,说明它的属性设计有问题;如果某张表在类图里找不到来源,说明类图漏了。

-- 航班表:对应类图中的 Flight,状态字段与状态图取值保持一致 CREATE TABLE t_flight ( flight_no VARCHAR(16) PRIMARY KEY, -- 业务主键,与类属性同名 from_city VARCHAR(32) NOT NULL, to_city VARCHAR(32) NOT NULL, depart_time DATETIME NOT NULL, seat_total INT NOT NULL, seat_sold INT NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'OPEN' -- OPEN/CLOSED/DEPARTED... ); -- 订单表:对应类图中的 Ticket,passenger_snapshot 用 JSON 存快照 CREATE TABLE t_ticket ( ticket_id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, flight_no VARCHAR(16) NOT NULL, amount DECIMAL(10,2) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'UNPAID', printed TINYINT(1) NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_ticket_flight FOREIGN KEY (flight_no) REFERENCES t_flight(flight_no) );

两点说明:seat_sold放在航班表而不是单独建余票表,是因为这个系统的并发量假设不高,用行锁更新即可;printed用布尔标记而不是另建打印记录表,是因为打印只是一次性动作,不需要审计。如果需求里出现了“补打票据需要记录次数”,那就得拆表——需求变了,模型就得跟着变,这才是建模该有的反应速度。

4. 动态行为建模:时序图、协作图、状态图怎么分工

4.1 时序图:登录、查询、订票、退票四张主线图

时序图不是画得越多越好,而是要把“有分支、有跨对象协作”的用例画透。这个系统里必须单独成图的是四张:用户登录、航班查询加订票、退票、查询并修改个人信息。登录、注册、权限验证这类会被反复引用的公共流程,单独建图后用引用框嵌入,避免每张图都重复画一遍。

以退票为例,对象从左到右依次是:注册用户、订票控制器、订单、航班、数据库。消息序列可以写成这样一张可核对的表,画图时按行往下走就行:

序号发送方接收方消息说明
1注册用户订票控制器refund(ticketId)点击退票按钮
2订票控制器订单findById(ticketId)校验订单归属
3订单数据库select返回订单记录
4订票控制器航班isRefundable()对应前置条件“航班未发出”
5订票控制器订单setStatus(REFUNDED)更新订单状态
6订票控制器航班releaseSeat()座位回滚,这一步最容易被漏
7订票控制器注册用户退票成功后置条件

第 6 步是整张图的得分点。很多方案画到第 5 步就结束了,评阅人一问“座位数怎么不加回去”就答不上来。写的时候记住一条:凡是改变了某个计数字段,时序图里必须有一条对应消息。

4.2 从时序图到协作图:等价转换与取舍

协作图(现在多称通信图)和时序图在语义上是等价的,区别只在强调点:时序图强调时间顺序,协作图强调对象之间的链接关系。原始设计里给了“查询并修改个人信息”“用户订票与查票”两张协作图,这个选择是合理的——这两条用例涉及的对象多、链接复杂,用协作图看结构更清楚。

转换的时候有个机械办法:把时序图里每一条消息,按“链接 + 消息编号”的形式标到两个对象之间的连线上,编号用 1、1.1、1.2 这种层级编号表示嵌套调用。转换完要自查一遍:时序图里的对象一个不少、消息一条不多。如果两张图对不上,优先信时序图,因为它的时间顺序更难造假。

课程设计里不必所有用例都画协作图,挑两张对象最多的画就行,剩下的用一句“其余用例的协作关系与时序图等价”带过。从业者做正式设计文档时,我通常只保留协作图用时序图替代,除非团队里有明确要求。

4.3 状态图:给订单和航班各画一张

状态图要挑“有生命周期”的对象画,这个系统里最该画的是订单,其次是航班。

订单的状态流转是:待支付(UNPAID)→ 已支付(PAID)→ 已出票(ISSUED)→ 已使用(USED),中途可以走已退票(REFUNDED)。每个状态迁移上要标事件名,比如pay()、issue()、refund()、use()。关键约束写在迁移条件里:只有 UNPAID 能触发 pay,只有 ISSUED 能触发 refund,已使用状态是终态。这些约束和 3.2 节里Ticket类的方法一一对应,这就是静态图和动态图互相印证的地方。

航班的状态相对简单:计划(SCHEDULED)→ 开放售票(OPEN)→ 截载(CLOSED)→ 起飞(DEPARTED)→ 到达(ARRIVED),任何非终态都可以迁到取消(CANCELLED)。注意“截载”和“起飞”要分开,因为isRefundable()的判断依据是这两个状态的差异,如果合并成一个状态,退票规则就没法精确表达。

4.4 活动图:只画带分支的业务流程

活动图最容易画重复——很多人把时序图翻译一遍就成了活动图,那没有意义。活动图的价值在分支和并发,所以这个系统里我只建议画一张:订票主流程,从“输入查询条件”开始,经过“是否有匹配航班”判断,分成“有余票 → 生成订单”和“无余票 → 提示余票不足”两条路,最后汇合到“进入支付页”。

如果需求里出现“多人同时订同一航班”,活动图里可以用分叉汇合表示并发抢座,配合一句“座位扣减使用行锁”,这张图立刻就具体了。没有并发需求的课设就别硬画泳道,画了反而暴露理解不到位。

5. 建模避坑:五处最容易翻车的地方

5.1 现象:用例规约写成散文,退票条件无处可查

原因通常是写规约时把注意力放在“用户点了什么”上,忘了写“系统在什么条件下不允许”。结果是规则只存在于作者脑子里,类图和时序图都没有承接它的地方。

解决方法是给每条规约强制加一栏“业务规则”,并且规定每条规则必须在类图里有一个方法或字段对应。比如“航班未发出才可退票”对应Flight.isRefundable(),“未订任何机票不能退票”对应订单查询返回空时的异常分支。加完这一栏再回头查,你会发现有些用例根本没法落地,那就是需求本身没想清楚。

5.2 现象:时序图里让数据库当参与者,还画了 SQL

原因是对“参与者”和“系统内部对象”的边界没有概念。数据库是持久化设施,不是业务协作者,把它画成生命线,图会变成“控制器 → 数据库 → 控制器”的来回,业务逻辑全丢了。

解决办法是把数据库降级为控制器的一个调用终点,用一条save/select消息带过,让订单、航班这些业务对象承担主要消息。真正要画出来的是“谁判断、谁改状态、谁回滚计数”。如果担心评审看不出持久化,可以在图下加一行说明,比画一堆 SQL 有效得多。

5.3 现象:类图穷举字段,类变成了数据表

常见于把每张表的每个字段都搬进类图,连create_time、update_time、is_deleted都画上,图占到两页还看不清关系。原因是把类图当成了建表文档。

解决办法是只保留业务含义明确的属性,审计字段统一用一句注释带过。判断标准很简单:这个字段会不会在时序图的消息参数里出现?会,就留;不会,就归到“持久化字段”里集中说明。类图是给人看结构的,不是给 ORM 生成代码用的。

5.4 现象:三张用例图和 16 条规约对不上

具体表现是管理员用例图里没有“航班查询”,但用例 3 的参与者写的是“注册用户、系统管理员”;或者规约里写了 16 条,图上只找到 14 个椭圆。原因多半是图先画、规约后补,两边各写各的。

解决办法是建一张对照表,一行一条用例,列出它在哪张图里出现、在哪个类上落地、在哪张时序图里展开。这张表填不满,就说明图集有洞。我在交付前一定会通读一遍这张表,填不出来的格子比任何评审意见都准。

5.5 现象:状态图和活动图画成了同一张图

表现是状态图里出现了“判断框”和“分叉”,活动图里出现了“状态”和“迁移事件”。原因是对两者的语义区别不清楚:状态图描述一个对象自身的状态变迁,活动图描述一个流程的控制流。

解决办法是问一句“这张图的主语是谁”。主语是一个对象(订单、航班),画状态图;主语是一个流程(订票、退票),画活动图。这个判断方法简单但管用,能省掉大量来回改图的时间。

6. 验收与答辩:用一条用例把十六张图串成证据链

交付前的最后一关,不是把图再美化一遍,而是挑一条用例从头走到尾,看能不能一路走通。我通常挑“退票”,因为它同时牵扯用户、订单、航班、数据库四个对象,还带业务规则和状态迁移,最容易暴露裂缝。

具体走法是四步。第一步,在用例规约里找到退票的前置条件“航班未发出”和异常流“用户未订任何机票”;第二步,到类图里确认这两条规则分别落在Flight.isRefundable()和订单查询的空值判断上;第三步,到时序图里数消息,确认“查订单 → 校验归属 → 查航班状态 → 改订单状态 → 回滚座位 → 返回结果”这六步一步不缺;第四步,到状态图里确认 REFUNDED 这个状态是从 ISSUED 迁过来的,而不是从 UNPAID 直接跳。四步全对,这条用例就算闭环了。

下面这张检查表可以照着过一遍,每一项后面填“是/否”,出现“否”就回去改图,不用改文档措辞:

检查项判断方法不合格信号
用例全覆盖规约条数 = 图中椭圆数(含 include 展开)图上有、规约无,或反之
参与者一致每条用例的参与者与图中连线一致管理员图缺查询类用例
规则落地每条业务规则在类图有方法或字段规则只写在规约文字里
消息完整每个改变计数的操作在时序图有消息只改状态、不回滚座位
状态闭合状态图每个状态都有入边和出边(终态除外)出现孤立状态
命名统一类名、表名、字段名同一套词图里叫 Ticket,表里叫 Order

答辩环节最常被问的三个问题,提前准备好答案:为什么个人信息要单独成类而不是用户类的属性;为什么座位不建类;退票和取消订单的区别在哪。前两个在 3.1 节里已经有答案,第三个的答法是“退票是用户主动发起且受航班状态约束,取消订单是系统侧的异常处理,两者状态迁移路径不同”。

我早年交第一版图集的时候,就是栽在“图很漂亮但串不起来”上——十六张图各自都对,被问一条完整链路就散了。从那以后我每次画完最后一张图,都强制拿一条用例从规约走到状态图,走不通就不算完。希望帮到你。

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