简介:围绕航空订票系统的UML建模设计PDF文档,适合软件工程专业学生、UML初学者以及正在完成课程设计或毕业设计的读者使用。文档以网上机票预订为业务背景,完整覆盖新用户注册、用户名可用性检查、密码一致性校验、登录验证、客户与管理员不同权限控制、航班信息查询、机票预订、退票及管理员安排航班等核心场景,并描述了系统与外部信用评价系统的交互逻辑:用户频繁退票会降低信用等级,信用过低则禁止继续购票。设计部分按照标准UML视图依次展开,包括用例图、类图、包图、顺序图、协作图、状态图、活动图、组件图和部署图;其中类图刻画了订票系统、已登录用户、未登录用户、管理员、评价系统等核心类及其属性与操作,包图则展现业务逻辑与用户信息、管理员信息、购买业务之间的依赖关系,各图均结合实例说明,帮助读者将抽象建模概念对应到具体业务功能,形成完整建模思路。资源为单个PDF文件,整体大小约2.04MB,图文结合、结构完整,便于直接阅读、打印或作为设计参考;目前已有1958人学习下载,适合用作UML建模实战案例、系统设计文档模板及答辩资料。
1. 一份航空订票系统的 UML 建模设计,到底要交付什么
软考中级里的 UML 建模设计,航空订票系统是被翻牌最多的题目之一;每学期的课程设计里,以《UML-建模设计-航空订票系统》为名的 PDF 更是常客。照着这类 PDF 复现过的人大多有同一种体验:用例图画得铺满一页,类图关系密得像蜘蛛网,可一到写代码,仍然不知道控制器该调哪个服务、订单状态用什么字段保存。这不是画图的问题,是 UML 建模没跟落地上挂钩。
UML 建模设计在航空订票系统里的真正作用是两件事:一是把需求边界钉死,让旅客、票务员、管理员之间的业务边界不再含糊;二是把动态行为说清楚,让订单从待支付到已出票再到已退款的每一步都有依据。这篇笔记按最常见的交付顺序,从用例图一路走到类图、时序图、活动图、状态图,补齐参数、关系和踩坑点,适合正在准备软考中级 UML 建模、或要独立完成订票系统建模设计的人。
2. 从角色到用例:把需求边界钉死的 UML 用例图
2.1 角色与用例的识别:别把功能清单当用例图
做 UML 用例图的第一步不是开画图工具,而是先回答一个问题:系统要服务谁。航空订票系统里的角色可以分成三类:第一类是直接使用业务功能的自然人,最常见的是旅客和票务员;第二类是后台管理者,也就是航班管理、航线定价的管理员;第三类是外部系统,比如支付网关、航信接口,它们不是人,但参与用例交互。很多课程设计只画了旅客和管理员两个角色,把支付网关漏掉,结果支付用例没有参与者,评审一眼就能看出来模型不完整。
识别用例时最容易翻车的做法是把功能清单当用例。登录、修改密码、发送验证码这些是系统功能,但不是业务用例;对订票系统而言,真正值得画出来的用例是查询航班、预订机票、在线支付、取消订单、申请退票、办理值机、航班管理这样能产生可观察业务结果的单元。判断标准很简单:这个用例执行完后,系统状态或参与者的目标是不是发生了变化。如果答案是否定的,它就该降级为某个用例的步骤或前置条件。
另一个常见问题是粒度。查询航班和“按出发日期筛选航班”不是两个用例,后者是前者的扩展路径;“预订一张机票”和“为多人同时预订”可以合并成一个用例,区别在流程分支里。粒度控制在用例建模里没有绝对标准,我的经验是:一个用例的主事件流能写满一页 A4 的步骤就够了,再长就要拆。软考中级里对 UML 用例图的评分也看重这一点,主动把粒度控制在业务目标层级,比堆二十个用例更稳。
2.2 用 PlantUML 画用例图:include 与 extend 的方向别画反
用例图用文本画有一个额外的好处:图和代码一起进 Git,评审意见可以直接指向具体行。下面这段 PlantUML 是常见做法里最简的一组角色与用例,旅客操作查询、预订、取消、退票,票务员代客下单,管理员维护航班,支付网关参与支付环节:
@startuml left to right direction skinparam rectangleStyle roundRect actor 旅客 as P actor 票务员 as Agent actor 管理员 as Admin actor 支付网关 as PayGw rectangle 航空订票系统 { usecase "UC01 查询航班" as UC01 usecase "UC02 预订机票" as UC02 usecase "UC03 在线支付" as UC03 usecase "UC04 取消订单" as UC04 usecase "UC05 申请退票" as UC05 usecase "UC06 航班管理" as UC06 } P --> UC01 P --> UC02 P --> UC04 P --> UC05 Agent --> UC02 Admin --> UC06 PayGw --> UC03 ' 基用例指向被包含用例,方向不要画反 UC02 ..> UC01 : <<include>> UC02 ..> UC03 : <<include>> ' 扩展用例指向基用例,表示可选分支 UC05 ..> UC02 : <<extend>> @enduml这段代码里,UC02 到 UC01 是 include,表达“预订机票”内部必然包含“查询航班”这一步;UC02 到 UC03 也是 include,表达下单后要走支付才算完成预订。UC05 到 UC02 是 extend,表达“申请退票”不是每次预订都会触发,而是特定条件下对预订流程的扩展。注意箭头方向:include 和 extend 都是虚线箭头,但方向完全相反——include 从基用例指向被包含用例,extend 从扩展用例指向基用例。画反是用例图评审里最高频的返工点。
代码里 left to right direction 让用例横向排布,避免矩形太扁;skinparam 只是控制外观。实际项目里我会把每个用例的编号写进名字,比如 UC02,这样和下面用例规约表能一一对应,评审时不用对着图猜。如果你用的是 StarUML 或 draw.io,关系画法相同,只是没有这种可 diff 的文本源文件。
如果想练习,可以自己把 UC04 取消订单的流程拆一下:取消发生在支付前还是支付后,流程完全不同。支付前取消就是一个独立用例;支付后取消通常走退票,这就要考虑是从 UC04 extend 到 UC02,还是单独画一个退票用例。这个判断过程比画图本身值钱。
2.3 用例规约:一张能进评审的用例表长什么样
光有用例图过不了评审,因为图里看不到前置条件、主事件流和异常分支。订票系统这类强流程业务,每个用例都要补一张用例规约表,至少包含用例编号、名称、参与者、前置条件、主事件流、备选事件流和后置条件。以 UC02 预订机票为例:
| 项目 | 内容 |
|---|---|
| 用例编号 | UC02 |
| 用例名称 | 预订机票 |
| 主要参与者 | 旅客、票务员,支付网关为次要参与者 |
| 前置条件 | 存在可预订的航班;旅客已登录 |
| 主事件流 | 1. 旅客查询并选择航班;2. 填写乘机人信息;3. 系统校验乘客与航班;4. 生成订单(状态:待支付);5. 跳转支付并等待回调;6. 支付成功,系统出票 |
| 备选事件流 | 2a. 乘机人信息不完整,提示补全;5a. 支付超时或失败,订单关闭并释放座位;6a. 出票失败,订单标记异常并人工介入 |
| 后置条件 | 订单状态为已出票;机票号已生成;库存扣减 |
这张表的价值在于它可以逐条对到后面的时序图和状态图:主事件流的第 5 步对应时序图里的支付交互,备选事件流的 5a 对应状态图里的“待支付 → 已取消”。如果用例规约里写的是支付成功后才出票,那么类图和时序图就不该出现先出票后支付的分支,这一条是后面做一致性检查的锚点。
对外部系统的处理也要在用例规约里写清:在线支付的主要参与者虽然是旅客,但系统必须和支付网关交互,所以参与者一栏要把支付网关列为次要参与者,并在主事件流里写明回调。软考中级 UML 建模的案例分析里,这种参与者划分和事件流标注是明确的给分点,别省。
3. 类图与数据落地:把抽象对象变成可建表的模型
3.1 核心类如何划分:五张实体表撑起一个订票系统
类图是 UML 建模设计里最容易画成“数据库 ER 图”的部分,这个方向不算错,但类图要承担的东西更多:它还要表达行为入口和对象间的归属关系。航空订票系统的核心类,我用常见做法里的最小集合来拆:Passenger(乘客)、BookingOrder(预订订单)、Ticket(机票)、Flight(航班)、Payment(支付记录)。五个类对应五类稳定的业务概念,页面上的输入框、按钮不应该直接映射成类。
类图代码示例如下:
@startuml class Passenger { -id: long -name: string -phone: string } class BookingOrder { -orderNo: string -status: string -createdAt: datetime } class Payment { -payNo: string -amount: BigDecimal -payTime: datetime } class Ticket { -ticketNo: string -seat: string -checkedIn: boolean } class Flight { -flightNo: string -departure: string -arrival: string -depTime: datetime -price: BigDecimal } ' 关联:乘客与订单长期引用 Passenger "1" -- "0..*" BookingOrder : 下单 ' 聚合:支付记录用于对账,不随订单销毁 BookingOrder "1" o-- "0..1" Payment : 支付 ' 组合:订单删除,票随之作废 BookingOrder "1" *-- "1..*" Ticket : 拆票 Flight "1" -- "0..*" Ticket : 承运 @enduml这里有三处关系直接决定了数据怎么落:Passenger 到 BookingOrder 是普通关联,一个乘客可以有 0 到多个订单;BookingOrder 到 Payment 是 0..1,一个订单最多一条成功支付记录,失败重试不在这里积累成多行;BookingOrder 到 Ticket 用了组合(*--),一个订单至少拆出一张票,订单取消时票随之作废。Flight 和 Ticket 之间也是关联,航班不因为某张票被退而消失。
注意属性里没有直接写外键字段,这是类图区别于表结构的关键。类图表达的是业务对象和职责,外键是关联关系在物理表里的落地方式。硬把 passengerId、orderId 塞进属性列表,除了让图变丑,没有任何信息增量。
实际项目里还可以加 User 和 Admin 做账号体系,但订单里存的是乘机人信息,和系统登录账号是两份数据。乘客可以没有账号也完成购票(比如票务员代购),把这两个概念合并成一个类,后面会立刻发现登录和值机的权限逻辑纠缠不清。建模初期宁可少画几个类,把已有类的职责边界画清楚,后面补类比删类容易得多。
3.2 UML 类图箭头含义:关联、聚合、组合与多重度怎么读
热词“uml类图箭头含义”在订票系统里几乎每个关系都能碰到。先给一张速查表:
| 关系 | 表示法 | 语义 | 订票系统示例 |
|---|---|---|---|
| 关联 | 实线 | 对象间存在长期引用 | Passenger — BookingOrder |
| 聚合 | 空心菱形实线 | 整体与部分可分离 | BookingOrder ◇— Payment(可独立对账) |
| 组合 | 实心菱形实线 | 部分随整体同生共死 | BookingOrder ◆— Ticket |
| 依赖 | 虚线箭头 | 一个类使用另一个类 | OrderController - - > OrderService |
| 继承 | 空心三角实线 | 一般/特殊关系 | 普通旅客和会员都继承 Passenger |
| 实现 | 空心三角虚线 | 实现接口 | 支付服务实现 PaymentGateway 接口 |
表里有一行需要特别解释:BookingOrder 和 Payment 到底用聚合还是关联,业界没有统一答案。我的判断标准是删除语义:删掉订单后,支付记录是否还有保留价值?在真实系统里支付记录要留着对账,所以我会把 Payment 画成聚合,整体(订单)删除了,部分(支付记录)依然独立存在。而 Ticket 不同,订单删掉,票直接失去存在意义,所以用组合。
多重度的读法也有固定规则:多重度写在关联线靠近哪一类的一端,就表示另一端一个实例对应这一端多少个实例。回到 3.1 的类图,Flight "1" -- "0.." Ticket,读作:一张机票对应一个航班,一个航班对应 0 到多张机票。如果有人写成了 Flight "0.." -- "1" Ticket,语义就变成一张机票对应 0 到多个航班,这在航空订票系统里就是重大建模错误。
聚合和组合这两个概念,是 UML 建模设计里被问得最多的地方,也是软考中级容易出判断题的考点。别背定义,用“删掉整体,部分要不要跟着删”来判断,几秒钟就能得到结论。
提示:如果用例规约里订单支持多个乘机人,请在类图里补上 Ticket 与 Passenger 的关联,再落表。不要为了让图好看而牺牲业务事实。
3.3 从类图到建表语句:映射规则与三个常见偏差
类图评审通过后,下一步就是把它翻译成建表语句。映射规则按常见做法可以概括成三条:一个类落一张表;一对多关联把外键放在“多”的那一端;0..1 关联的外键允许为空或用独立表加唯一约束。下面这段 SQL 对应 3.1 的五个类,拆票关系落成 ticket.order_id,支付关系落成 payment.order_id 唯一约束:
-- 关联关系映射自类图:Passenger "1" -- "0..*" BookingOrder CREATE TABLE passenger ( id BIGINT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20) ); CREATE TABLE booking_order ( id BIGINT PRIMARY KEY, order_no VARCHAR(32) UNIQUE NOT NULL, passenger_id BIGINT NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, CONSTRAINT fk_order_passenger FOREIGN KEY (passenger_id) REFERENCES passenger(id) ); -- 聚合关系:payment.order_id 加唯一约束,保证一个订单最多一条成功支付记录 CREATE TABLE payment ( id BIGINT PRIMARY KEY, pay_no VARCHAR(32) UNIQUE NOT NULL, order_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, pay_time DATETIME NULL, CONSTRAINT fk_payment_order FOREIGN KEY (order_id) REFERENCES booking_order(id), CONSTRAINT uk_payment_order UNIQUE (order_id) ); CREATE TABLE flight ( id BIGINT PRIMARY KEY, flight_no VARCHAR(10) NOT NULL, departure VARCHAR(50) NOT NULL, arrival VARCHAR(50) NOT NULL, dep_time DATETIME NOT NULL, price DECIMAL(10,2) NOT NULL ); -- 组合关系:订单删除,票记录级联处理 CREATE TABLE ticket ( id BIGINT PRIMARY KEY, ticket_no VARCHAR(20) UNIQUE NOT NULL, order_id BIGINT NOT NULL, flight_id BIGINT NOT NULL, seat VARCHAR(4) NULL, checked_in TINYINT DEFAULT 0, CONSTRAINT fk_ticket_order FOREIGN KEY (order_id) REFERENCES booking_order(id), CONSTRAINT fk_ticket_flight FOREIGN KEY (flight_id) REFERENCES flight(id) );四个点容易出错,逐个说。
第一,金额类型不要用 float 或 double。票价、支付金额这类钱有关的字段,业务上要求精度可控,DECIMAL(10,2) 才能保证对账不差一分钱。类图里写 BigDecimal,到 SQL 落 DECIMAL,这是一个很具体的映射约定。
第二,订单状态 status 一定会出现在 booking_order 表里,它的取值边界由后面第 4 章的状态图定义。常见偏差是只建表不定义状态流转,后端的订单表逐渐长出十几个魔法数字,等你想做售后统计的时候已经分不清哪个状态是终态。
第三,乘机人和下单人不一定是同一个 passenger_id。类图里我把 Passenger 和 BookingOrder 画成一对多关联,但实际业务中一张订单里可以有多个乘机人,所以更严格的设计是 Ticket 与 Passenger 再建立关联,订单里只保存联系人。上面 SQL 为了保持与类图一致,省略了乘机人维度;真实项目里请按这个差异补一张 ticket_passenger 关联或直接把 passenger_id 放到 ticket 表。
第四,代码生成时不要被这张表锁死。类图表达的是稳定关系,建表语句是物理实现。如果后续业务引入“中转联程”,一个订单对应多个航班,类图要改成 BookingOrder 与 Flight 的多对多关联,新增 OrderSegment 类,而不是在 ticket 表里硬塞多个 flight_id。
4. 动态行为建模:时序图、活动图与状态图怎么配合
4.1 预订主流程的时序图:对象间消息别画成流程图
动态结构图处理的是“事情怎么发生”,在航空订票系统里,UML 中的动态结构图包括时序图、活动图、状态图和通信图,最常用的是前三种。先看预订机票的主流程时序图,它的目标是回答:一笔订单从创建到出票,消息在哪些对象之间传递、顺序如何、谁在等待谁。我一般不会把页面、控制器、数据库表全部拉进来,而是只留四个协作对象:订票页面/Controller、OrderService、PaymentService、TicketService。
@startuml actor 旅客 participant "订票页面" as UI participant "OrderController" as C participant "OrderService" as S participant "PaymentService" as P participant "TicketService" as T 旅客 -> UI: 提交航班与乘机人 UI -> C: POST /orders C -> S: createOrder() S --> UI: 返回订单号 旅客 -> UI: 确认支付 UI -> C: POST /orders/{orderNo}/pay C -> S: pay(orderNo) ' 支付网关回调入口,由 PaymentService 接收 S -> P: requestPayment(orderNo) P --> S: 支付回调结果 S -> T: issueTickets(orderNo) T --> S: 返回票号 S --> UI: 出票成功 UI --> 旅客: 展示电子票 @enduml时序图的读法非常直观,实线箭头是同步调用,虚线箭头是返回或回调。代码里支付网关在第一版用例图里是外部参与者,到时序图这一层就体现为 PaymentService 与支付网关之间的回调,而不是旅客付完钱系统自动跳过,这一点要在画之前想清楚。
画时序图最常见的坑是把它画成流程图。活动图的节点是“动作”,时序图的节点是“对象”;如果有人把“判断订单是否有效”“校验库存”也画成一条条生命线,图会瞬间失去可读性。时序图只表达消息序列,判断逻辑由活动图或代码内部承担。对象太多时合并为一层服务,对象太少时把数据库单独画出来也行,但订票系统这类业务,Controller-Service 两层足够看清主流程。
另外,返回消息不能全画。很多人习惯每个方法调用都画返回箭头,图马上密成蛛网。我一般只画有业务意义的返回:返回订单号、返回出票结果;普通赋值类返回省略,代码里能看明白。
4.2 活动图处理支付与出票:并行分支的取舍
支付和出票流程里有明显的分支与并行,这是活动图最擅长表达的场景。订单支付成功后,常见做法是并行执行两件事:给乘客生成电子票、更新订单状态并发送通知;支付失败则进入另一条分支。下面的活动图把这两个分支画在一个模型里:
@startuml start :接收支付回调; if (回调验签通过?) then (是) if (金额与订单一致?) then (是) fork :生成电子票; :发送乘机通知; fork again :更新订单状态为已支付; endfork :返回出票结果; else (否) :标记支付异常; :挂起订单等待人工处理; endif else (否) :丢弃回调并记录日志; endif stop @enduml活动图里 fork 与 endfork 之间的多个节点表示并行执行。实际代码里生成电子票和发送通知并不需要严格同时,但模型上它们之间没有先后依赖,所以画并行;如果把“发送通知”画在“生成电子票”之后,等于暗示通知一定等票生成完,代码就会按这个顺序写,白白加一次无谓的等待。
要不要给活动图画泳道,我的建议是:交付文档里画,开发沟通时不画。泳道能分清 OrderService、PaymentService、TicketService 各自负责哪些动作,但会让图宽出一倍。先用不带泳道的版本把分支逻辑评审完,再在终版文档里补泳道,效率最高。软考中级 UML 建模的案例分析经常拿这种图考“哪些节点可并行”,平时把 fork 用对很加分。
活动图的粒度同样要控制:意图是表达“支付成功后系统干什么”,而不是把每行代码都搬上来。像“生成订单号”“组装请求报文”这类实现细节,画进去只会让评审重点跑偏。
4.3 订单状态图:把状态机画完整,代码少一半返工
订单状态是航空订票系统的核心状态机,必须单独画状态图。状态图里每一个矩形是一个稳定状态,每一条迁移线上的文字格式是“事件 [守卫条件] / 动作”。我从常见设计里抽一版可靠的状态集合:待支付、已支付、已出票、已取消、已使用、已退款。
@startuml state "待支付" as PENDING state "已支付" as PAID state "已出票" as TICKETED state "已取消" as CANCELLED state "已使用" as USED state "已退款" as REFUNDED [*] --> PENDING: 提交订单 ' 迁移必须写事件与守卫条件,否则代码无法判断合法性 PENDING --> PAID: 支付成功 [金额与订单一致] / 记录支付 PENDING --> CANCELLED: 支付超时或用户取消 PAID --> TICKETED: 出票成功 PAID --> REFUNDED: 退票申请通过 [起飞前24小时] / 原路退款 TICKETED --> USED: 完成值机并乘机 TICKETED --> REFUNDED: 退票申请通过 / 回收座位 USED --> [*] REFUNDED --> [*] @enduml这段状态图里有几个细节直接影响代码。第一,PENDING 有两个出口,支付成功和取消分别收敛到不同终态,状态枚举必须把这两个终态都定义出来,否则后端只能用 is_deleted 这种补丁。第二,退票迁移带了守卫条件“起飞前 24 小时”,说明不是所有已支付订单都能退,映射到代码就是状态迁移前的一段校验逻辑,不该散落在 controller 里。第三,状态图没有从 TICKETED 回到 PAID 的迁移,因为改签在业务上要拆成退票加新订两笔,而不是直接改状态。
画状态图时有一个值得养成的习惯:先用它推导异常分支,再去补用例图。比如“支付超时”这条迁移,用例图 UC04 取消订单的备选事件流里就要有对应描述;如果状态图里有用户取消、系统超时取消两种来源,用例图也要能区分。状态图不是独立交付物,它是把散落在用例规约里的分支约束收敛成了唯一事实源。
状态命名也要和代码里的枚举统一。建模时用“待支付”,代码里就不要写 status=0 这种魔法数字,更不要出现两个含义重叠的中间状态,比如“已支付待出票”和“支付成功处理中”。如果状态图里有重叠状态,说明状态划分还不够稳。
5. UML 建模常见问题排查:照图写代码前先过的五道关
下面五条是我在做订票系统 UML 建模和把它翻译成代码时反复踩过的坑,每条都按现象、原因、解决三段写,照着排查至少能省下一轮评审返工。
5.1 include/extend 画反:用例评审被当场打回的翻车现场
现象:用例图里出现“在线支付 ..> 预订机票 : < >”这种画法,评审问一句“支付包含预订?”直接翻车。
原因:把 include 理解成“谁依赖谁”,而 UML 的定义里 include 表示基用例的执行必然包含被包含用例,方向是从基用例指向被包含用例;extend 表示扩展用例在特定条件下插入基用例,方向是从扩展用例指向基用例。
解决:先找“没有这一步,主流程还能不能成立”。预订机票必须有查询这一步,所以预订 include 查询;支付是预订主流程的一部分,所以预订 include 支付;退票不是每次预订都发生,所以退票 extend 预订。画完后指着每条虚线把这句话说一遍,能说通再提交。
5.2 多重度写反:双向关联的读法只有一种
现象:类图上写着 Flight "0..*" -- "1" BookingOrder,代码评审时发现开发者按“一个订单对应多个航班”实现了查询,查出脏数据。
原因:多重度位置的读法错了,以为靠近哪个类就属于哪个类。
解决:多重度写在线的一端,读的时候先固定另一端。Flight "1" -- "0.." BookingOrder 的正确读法是:一个订单对应 1 个航班(靠近 Flight 的 1),一个航班对应 0 到多个订单(靠近 BookingOrder 的 0..)。反过来写就成了一个订单对应多个航班。建模完成后专门抽 5 分钟把每条关联的多重度按这个读法过一遍,比 code review 才发现问题便宜得多。
5.3 聚合与组合混淆:用“删除语义”一测就准
现象:把 Passenger 和 BookingOrder 画成组合,类图意思变成订单跟着乘客一起销毁,后来做乘客注销功能时订单全没了。
原因:看到“一个乘客名下有很多订单”就自然想到整体-部分,忽略了组合要求部分不能脱离整体独立存在。
解决:用删除语义测试。删掉乘客,订单还要不要留?要留,所以是关联;删掉订单,机票记录是否还在?实际业务里订单删除后票要作废或退掉,票失去存在意义,所以订单与票是组合。这个测试对聚合也适用:聚合中部分可以独立存在,比如支付记录离开订单也能用于对账。记住这一个标准,就不用再背概念。
5.4 状态图有状态没有事件:补丁式状态机的源头
现象:状态图只写了“待支付 -> 已支付 -> 已出票”,没有一条迁移上写触发事件;代码里 updateStatusById 被到处调用,后来加了三个状态没人说得清谁合法。
原因:把状态图当成了“流程简笔画”,忽略了状态迁移的合法性由事件和守卫条件保证。
解决:每条迁移必须写成“事件 [守卫条件] / 动作”的完整格式。比如“支付成功 [回调校验通过] / 记录支付流水”,“退票 [起飞前24小时] / 原路退款”。画完再自查一遍:没有写事件名的迁移,一律补上;没有守卫条件的非法迁移,比如从已使用退回已支付,直接删掉。状态图否则就会变成事后记录,而不是事前约束。
5.5 时序图与用例图互相对不上:一致性走查清单
现象:用例规约主事件流写“支付成功后出票”,时序图里 TicketService 却在支付前出票;用例图里没有外部系统,时序图里突然冒出航司接口。
原因:用例图、类图、时序图往往由不同阶段完成,没人做一致性走查,各图各说一套。
解决:交付前做一次 15 分钟的走查,按三点查:一是每个用例的主事件流,至少能在某张时序图里找到对应消息序列;二是时序图里出现的每个外部参与者,在用例图里要有对应 Actor;三是状态图的每个终态,要能对应到用例规约的后置条件或备选事件流。这三条都过,再用代码去实现,基本不会出现模型推翻重来的情况。我把这张检查清单贴在文档目录第一页,每次建模评审都能少一轮返工。
6. 把 UML 建模落成可验收的交付物:一致性检查清单与脚本
UML 建模设计做到最后,交付物不是那张 PDF 里的图片,而是一套能互相印证、能翻译成代码的模型。我通常会把用例图、类图、动态图和建表语句放进同一个 Git 仓库,用 PlantUML 文本维护,并用下面这个命令批量编译校验:
plantuml -tsvg diagrams/*.puml这个命令能把目录下所有 .puml 源文件编译成 SVG,任何一个文件语法错误都会直接报出文件名和行号。把这条命令挂到提交前检查里,图就不会出现“文档里的图和代码不一致”的尴尬。这只是工程约束,真正决定 UML 建模设计质量的还是模型本身的一致性。
| 交付物 | 内容 | 验收标准 |
|---|---|---|
| 用例图与用例规约 | Actor、用例、主备事件流 | 每个业务目标有独立用例;每个用例规约后置条件明确 |
| 类图 | 核心类、关系、多重度 | 每个类能映射到表或值对象;关系多重度读法可验证 |
| 时序图与活动图 | 主流程消息、并行分支 | 用例主事件流在时序图中可追踪 |
| 状态图 | 状态、事件、守卫、动作 | 每个迁移有事件;终态对应用例后置条件 |
| 建表语句与核心服务 | DDL 和状态迁移方法 | 类图关系完整落到外键;状态机落到枚举与状态迁移逻辑 |
我的习惯是先把状态图画出来再写代码,订票这种强状态流转的业务尤其如此,状态图能提前暴露很多异常分支。这套流程帮我挡掉过至少两次上线前的返工。建模阶段多花半天把状态机钉死,后面少熬夜排查脏数据,这笔账怎么算都划算,希望帮到你。
本文还有配套的精品资源,点击获取