简介《航空订票系统 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() { ... } // 打印需 statusISSUED }参数说明上有个细节值得讲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... ); -- 订单表对应类图中的 Ticketpassenger_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 节里已经有答案第三个的答法是“退票是用户主动发起且受航班状态约束取消订单是系统侧的异常处理两者状态迁移路径不同”。我早年交第一版图集的时候就是栽在“图很漂亮但串不起来”上——十六张图各自都对被问一条完整链路就散了。从那以后我每次画完最后一张图都强制拿一条用例从规约走到状态图走不通就不算完。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?