资讯详情 航空订票系统UML建模:从用例规约到可落地接口与数据表
📅 2026/11/2 17:36:51
简介航空订票系统 UML 建模设计文档面向软件工程课程设计、UML 建模练习及中小型订票系统需求分析场景围绕游客与注册用户、管理员两类角色梳理订票、航班信息管理、个人信息管理三个核心模块的业务边界与权限差异。文档以需求说明为起点依次给出用户、管理员和顶层用例图并配合注册、登录、航班查询、机票预订、已订机票查询、退票、改签、打印票据等用例规约继而展开用户类、管理员类、航班信息类、机票类、个人信息类的类图设计以及登录、查询、预订、退票等状态图、顺序图和合作图便于读者理解从需求到静态结构、动态交互的完整建模链路。包内共1个docx文件约535KB内容集中于文字说明与图形化模型适合直接用于课程作业参考、答辩材料整理和建模思路复盘。已有1627人学习可作为航空订票系统UML设计的一份可对照案例。1. 文档定位:这不是一份交作业的图纸,而是一套能翻译成接口的建模底稿上周有人把这份航空订票系统 UML 建模设计.docx甩给我,问的第一句是里面一行代码都没有,怎么跑?。我把文档翻完,反倒觉得这问题问反了:这份航空订票系统 UML 建模设计里最值钱的部分,不是那几张用例图、类图、时序图画得齐不齐,而是它把 16 条用例规约的前置条件、基本事件流、异常事件流、后置条件一条条写全了。做过业务系统的人都知道,这部分写全了,接口清单基本就定了七八成。它的定位很明确:给一个订票子系统做完整的建模交底,覆盖游客、注册用户、系统管理员三类角色,拆成订票模块、航班信息管理模块、个人信息管理模块三块。适合两类人拿走用——想照着模型做一个练手项目的学生或转行者,以及手里有类似需求、缺一份角色权限和状态流转梳理的开发者。下面按能落地的顺序拆。2. 需求与用例拆解:16 条用例规约怎么变成可执行的功能清单2.1 三类角色的边界,决定了后面所有的权限代码文档开头的系统需求列了六条,但真正的骨架是角色划分:游客只能看首页,首页上有登录和注册两个入口;注册用户登录后能查航班、订票、退票、付款、打印票据、改个人信息;管理员做航班信息的增删查改和订票数据统计。这三层不是简单的 if-else,它直接决定了后面每一张表要带什么字段、每个接口要挂什么拦截。最容易漏的是管理员这条线。文档写得很清楚:管理员的注册属于特殊注册,由最高权限管理员分派标识符,或者用航空公司内部员工号做区分鉴定;管理员登录流程和普通用户一致,但点击添加航班信息这类敏感操作时,要再输一次当初被分派的标识符做二次验证。这一段非常关键,我后面把它落成了admin_credential一张独立表加一个二次校验中间件。很多人做这类系统会把管理员和用户合并成一张表加个role字段,能跑,但一旦要做标识符分派、要做敏感操作二次验证、要审计谁改了航班,字段就会一路膨胀到没法看。游客这个角色也常被当成废话忽略。它是有实际含义的:未登录请求必须被拦在首页之外,/api/flights/search这类接口在未登录时应该返回 401 而不是空列表。文档明确写了未注册用户只能访问该系统的首页,这句话对应的是路由白名单,不是文案。2.2 三张用例图为什么要分开画文档把用例图拆成顶层用例图、注册用户用例图、管理员用例图三张,而不是揉成一张大图。这个拆法是有工程理由的:一张图混两个参与者,连线会交叉成一团,评审的时候谁都看不清哪个功能归谁。分开画之后,每张图的参与者只有一个,权限边界一目了然。三张图的内容可以直接抄成功能清单:图类型参与者用例顶层用例图注册用户、系统管理员个人信息管理、订票管理、航班信息管理注册用户用例图注册用户注册、登录、航班查询、机票预订、已定机票查询、退票、修改已订航班个人信息、查看个人信息、修改个人信息、打印票据管理员用例图系统管理员注册、登录、添加航班信息、修改航班信息、删除航班信息、查看个人信息、修改个人信息、统计订票数据顶层图只画三个粗粒度用例,是给非技术方看的;两张角色图才是给开发看的。我一般拿到这种拆法,会先按顶层图的三大块建三个 Controller 包,再按角色图往里填方法,最后回到用例规约核对每个方法的入参和前置条件,顺序别反。2.3 用例规约的四段式,是最值得抄的部分16 条用例规约统一用了前置条件 / 基本事件流(参与者动作 vs 系统响应) / 其他事件流 / 异常事件流 / 后置条件的结构。这个结构看着像模板,但每条都填了实际内容,而不是三个字无敷衍过去。以用例 1 用户注册为例,异常事件流写得非常具体:未输入用户名、未输入密码、两次输入密码不同,分别对应三条不同的提示。这三条落到代码里就是三个校验分支,而且有先后顺序——先判空再判一致性,顺序反了会出现两次密码不同和密码为空同时报的尴尬。再说后置条件,注册成功跳转登录界面,这句话翻译过来就是注册接口不签发 token,用户必须重新登录一次。我见过不少实现图省事,注册直接返回 token 自动登录,和规约就不一致了。再看用例 5 已定机票查询,它的异常流是用户未订任何机票时返回该用户未订任何机票消息。这是一条很细的约定:查不到数据时返回提示信息,而不是返回空数组让前端自己去猜。同理用例 10 打印票据,异常流是用户未预定机票时提示,说明打印接口在生成 PDF 之前必须先查订单存在性。这两条合起来能推出一条规则:凡是查询类、凭据类用例,空结果要当异常流处理并给明确文案,不要静默返回空集合。2.4 从用例到路由表:16 条用例的映射清单把用例规约直接映射成接口,是这份建模文档最实用的一步。我按用例 ID 逐个对了一遍,整理出这样一张对照表(节选,只列会写代码的部分):用例 ID用例名建议路由前置校验2用户登录POST /api/auth/login用户名、密码非空3航班查询GET /api/flights?fromtodate查询条件非空4机票预订POST /api/orders航班存在且有余位5已定机票查询GET /api/orders/mine已登录,取 userId6退票POST /api/orders/{id}/refund航班未起飞9修改个人信息PUT /api/profile字段完整性校验10打印票据GET /api/orders/{id}/ticket订单存在且已支付11添加航班信息POST /api/admin/flights二次标识符验证14统计订票数据GET /api/admin/stats?fromto二次标识符验证映射的时候有两个点必须自己补,文档没写但绕不过去。一是用例 6 退票的前置条件写的是航班未发出,这个未发出到底怎么判,得有天字段落地,我一般用flight.departure_time now()加一个order.status白名单双重判定;二是用例 9 修改个人信息,规约里只说输入信息不符合验证要求就提示,没说什么算不符合,这部分要么在文档里补规则,要么在代码里写成常量表,绝不能散落在各处。3. 静态结构落地:类图里的实体划分与字段归属3.1 用户、管理员、个人信息,三者为啥要拆开摘要里列了用户类、管理员类、航班信息类、机票类、个人信息类。前三个是角色和资源,个人信息类是独立的一个,这个拆法我认为是对的。用户类存的是身份和登录凭据,邮箱手机号密码哈希;个人信息类存的是姓名、证件号、联系方式这类资料。两者是一对一,但生命周期不同——用户注册的瞬间就有记录,个人资料可能是空的,后续在修改个人信息用例里慢慢补。如果合并成一张表,注册流程就要么强制填一堆非必填项,要么留一堆 NULL 字段。管理员类同理。文档强调了管理员注册的特殊性,由最高权限管理员分派标识符。这意味着管理员不仅有 role 区别,还有一条谁给你派的标识符的溯源关系。我把它落成admin表加admin_credential表:前者存管理员的账号和角色,后者存标识符、分派人、生效时间、是否已使用。这样敏感操作二次验证就有地方查,审计也有据可依。3.2 航班、机票、订单,这三个概念千万别合并这是我在类似项目里踩过最疼的一个坑。文档里写的是航班信息类负责增删查改,机票类负责预订退票修改。如果照着字面把机票当成订单,后面一定会崩。正确的划分应该是三层:航班(Flight)是一趟具体的飞机,有航班号、起降机场、起飞时间、总座位数,由管理员维护;订单(Order)是一次订票行为,挂在某个用户和某个航班上,带状态、下单时间、支付信息;机票/座位(Ticket)是订单和航班之间的具体座位占位记录,一个订单可以有多张票(给同行人订)。为什么必须拆?因为退票这个用例的约束是航班未发出,它约束的是航班,不是订单;而统计订票数据统计的是订单和票数,不是航班数。三个概念混成一个实体,后面每加一条业务规则都要打补丁。3.3 类图转数据表:一份可以直接执行的 DDL照着上面的实体划分写出来的建表语句大概长这样,关键在状态字段和唯一约束:-- 用户表:只放身份与凭据,个人资料拆到 profile CREATE TABLE app_user ( id BIGSERIAL PRIMARY KEY, username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, role VARCHAR(16) NOT NULL DEFAULT USER, -- USER / ADMIN created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 个人信息表:与用户一对一,注册时可为空 CREATE TABLE profile ( user_id BIGINT PRIMARY KEY REFERENCES app_user(id), real_name VARCHAR(64), id_card VARCHAR(32), phone VARCHAR(32), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 航班表:管理员维护,departure_time 是退票判定的依据 CREATE TABLE flight ( id BIGSERIAL PRIMARY KEY, flight_no VARCHAR(16) NOT NULL, from_airport VARCHAR(8) NOT NULL, to_airport VARCHAR(8) NOT NULL, departure_time TIMESTAMPTZ NOT NULL, seat_total INT NOT NULL CHECK (seat_total 0), seat_taken INT NOT NULL DEFAULT 0, CHECK (seat_taken seat_total) -- 超卖在第一道防线就被挡住 ); -- 订单表:一次订票行为,状态机收敛在这里 CREATE TABLE ticket_order ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES app_user(id), flight_id BIGINT NOT NULL REFERENCES flight(id), status VARCHAR(16) NOT NULL DEFAULT PENDING_PAY, amount NUMERIC(10,2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_order_user ON ticket_order(user_id);这段 DDL 有三个地方是从用例规约反推出来的。第一,flight上的CHECK (seat_taken seat_total)对应航班存在时可订票,把超卖防守放到数据库层,应用层写漏了也不会真出问题。第二,ticket_order.status用字符串枚举而不是布尔,是因为状态图里明确有未预订、预订成功、退票成功等多个态,布尔存不下。第三,profile主键直接引用app_user.id,省掉一个自增列,也保证了一对一不会被破坏。参数说明一句:status的取值必须和后面第 4 章的状态迁移表严格对齐,新增状态时先改迁移表再改代码,别反过来。3.4 用 PlantUML 把类图版本化,别再交图片原文档里的图是贴进去的位图,改一个字段就得重新画。我在实际项目里会把类图写成 PlantUML 源码跟代码放一个仓库,评审时看 diff,比对着截图找差异靠谱得多:startuml class AppUser { Long id String username String passwordHash String role } class Profile { String realName String idCard String phone } class Flight { String flightNo Instant departureTime int seatTotal int seatTaken } class TicketOrder { Long id String status BigDecimal amount } AppUser 1 -- 1 Profile AppUser 1 -- 0..* TicketOrder Flight 1 -- 0..* TicketOrder enduml类之间的关系只有三条,但每条都对应一条业务规则:用户与资料一对一,用户在注册那一刻就存在;用户与订单一对多,一个用户能订多张票;航班与订单一对多,但订单上的status决定了它是否还占用座位余量。渲染命令一条就够:plantuml -tsvg docs/*.puml,产物直接进 CI,图永远不会和代码脱节。4. 动态建模:时序图与协作图怎么串起一次订票、退票4.1 登录与注册时序图,验证逻辑到底放哪一层文档单独给用户登录、用户注册、用户退票、添加航班信息等都画了时序图,理由是时序图中多次使用的模块单独建立时序图并引用。这个做法我认同——登录在几乎所有后续用例里都是前置条件,重复画六七遍纯属浪费。登录时序的参与者顺序大致是:用户 → 登录界面 → 认证控制器 → 用户表 → 返回结果。新手最容易把密码校验写在控制器里,然后 token 签发又写在另一个地方,最后出现校验通过但 token 里没有 role的问题。我的做法是把校验和签发收在同一个服务方法里,控制器只负责收参和返回:# auth_service.py def login(username: str, password: str) - str: # 1. 空值校验:对应用例 2 的异常流未输入用户名/密码 if not username: raise BizError(请输入用户名) if not password: raise BizError(请输入密码) # 2. 取用户,查不到与密码错用同一句提示,避免撞库探测 user repo.find_by_username(username) if user is None or not verify_hash(password, user.password_hash): raise BizError(用户名或密码错误) # 3. 签发 token,把 role 塞进载荷,供后续权限中间件读取 return issue_token(uiduser.id, roleuser.role)三个参数各有讲究:username判空要放在查库之前,否则空字符串也会走一次数据库;错误提示统一成一句,是因为分开提示等于把这个用户名存在白送出去;issue_token里的role必须写进去,不然管理员用例图里那些敏感接口没法在网关层拦。注册的时序同理,但多一条:注册成功后不签发 token,对应规约里注册成功,跳转登录界面的后置条件。这条后置条件看着像 UI 细节,实际是安全约定。4.2 订票与查票协作图:一次点击背后经过几个对象协作图强调的是对象之间的连接关系,文档给的是用户订票与查票协作图。把它翻译成调用链,大概是:用户 → 订票控制器 → 订单服务 → 航班服务(校验余量) → 订单仓储(落库) → 返回。这里必须自己补一个文档没画的东西:事务边界。查余量、扣余量、写订单这三步如果不是一个原子操作,并发下必然超卖。常见做法是用一条带条件的 UPDATE 把校验和扣减合并:-- 扣减余量:条件里带 seat_taken seat_total,影响行数为 0 即余票不足 UPDATE flight SET seat_taken seat_taken 1 WHERE id :flightId AND seat_taken seat_total; -- 返回 0 行 → 抛余票不足,整笔订单回滚这条语句把先查再改的窗口彻底关掉了。参数:flightId来自下单请求,seat_taken seat_total是并发安全的关键条件,少了它,两个请求同时读到seat_taken 99, seat_total 100,就会都成功扣到 101。血泪经验:凡是查一次再改一次的地方,都该想想能不能压成一条带条件的写。4.3 退票时序图上的那个约束,比代码本身重要用例 6 的前置条件写的是用户进入查询页面,基本流是用户点击退票按钮,系统根据订票信息 id 进行退票操作。真正的约束藏在系统需求第 3 条里:航班未发出才能退。所以退票时序应该是:用户 → 订单控制器 → 订单服务(取订单)→ 航班服务(取起飞时间)→ 判定 → 调用退款 → 更新订单状态 → 释放余量。判定放最前面,一旦超时就抛异常,后面的退款和释放都不执行。def refund(order_id: int, uid: int) - None: order order_repo.get(order_id, uid) # 只能退自己的单 if order is None: raise BizError(未找到该订单) flight flight_repo.get(order.flight_id) # 约束来源:系统需求第 3 条航班未发出 if flight.departure_time now(): raise BizError(航班已起飞,无法退票) if order.status not in (PAID, PRINTED): raise BizError(当前订单状态不支持退票) payment.refund(order) # 退款 order_repo.update_status(order.id, REFUNDED) flight_repo.release_seat(order.flight_id) # 释放余量,和扣减对称注意order_repo.get(order_id, uid)一定要把uid带进查询条件,不然改个 URL 里的 id 就能退别人的票,这是最典型的越权漏洞。释放余量的语句要和扣减对称,别一个用1一个用-1结果字段名打错。4.4 状态图落地:订单状态机写成一张迁移表状态图里列了登录、查询、预订、退票几种状态。登录和查询是会话态,用完即弃,真正的持久状态只有订单。按原文档的用例能推出的最小状态集是:当前状态触发动作目标状态允许的角色PENDING_PAY支付成功PAID注册用户PENDING_PAY超时/主动取消CANCELLED注册用户PAID打印票据PAID(不变)注册用户PAID航班未起飞,退票REFUNDED注册用户PAID航班已起飞PAID(拒绝)-这张表的价值在于,所有能不能做的判断都收敛到一处,代码里只写if (from, action) not in TRANSITIONS: raise。原文档的状态图覆盖了未预订/预订成功/退票成功,但没画已支付和已打印,所以实际实现时要把这两个补进去,否则退票判定会出现已支付算不算预订成功的歧义——这坑我在第 5 章单独说。5. 避坑排查:照着这份建模直接开工最容易翻车的 5 个地方第一条:管理员注册这一段没有闭环,权限表会缺字段。现象是管理员登录后,点添加航班信息时不知道该拿什么去校验二次身份。原因是文档只写了由最高权限管理员分派标识符,但用例规约里 16 条用例没有一条叫分派标识符,这部分是空的。解决方式是自己补一张admin_credential表,存标识符、分派人、有效期,并把分派这个动作作为第 17 条用例补进规约,否则永远落不了地。第二条:时序图里没有事务边界,订票会超卖。现象是压测时同一航班卖出比座位数多的票。原因是订票时序图画的是查余量 → 写订单两个独立交互,没有标注它们在一个事务里。解决办法是把校验压成一条带seat_taken seat_total条件的 UPDATE,影响行数为 0 就回滚,这一条比在应用层加锁省事得多。第三条:状态图少画了已支付,退票判定对不上。现象是代码里退票时判status BOOKED,但支付完成后状态变成了另一个值,导致已付款的用户退不了票。原因是原状态图只覆盖到预订成功,没列支付和出票。解决办法是按第 4.4 节那张迁移表把状态集补全,并约定状态名只在一处常量文件里定义,别在多个 Service 里各写各的字符串。第四条:用例图和类图的命名不一致,退票按钮找不到归属。现象是前端问退票调哪个接口,后端翻类图发现只有机票类,点进去是航班信息,订单在哪没人说得清。原因是文档里机票这个词同时指代了资源(座位)和凭证(订单)。解决办法是在类图评审时强制要求一个名词只对应一个实体,订单必须单独成类,退票动作挂在订单上而不是航班上。第五条:后置条件写成了 UI 状态,接口层没法判断成功。现象是大量用例的后置条件都是进入 XX 成功页面,写接口测试时不知道断言什么。原因是建模时把界面跳转当成了业务结果。解决办法是把后置条件改写成可验证的数据结果,比如订单状态变为 PAID 且 flight.seat_taken 加 1,页面上跳哪是前端的事。这条改完之后,接口测试用例几乎可以自动生成。提示:这些坑有一个共同点——它们都不是代码写错了,而是建模阶段的状态、实体、边界没画全。建模文档省下来的每一句,后面都会变成一次线上排查。6. 二次开发:把 docx 改造成能跑起来的工程骨架拿到这类文档之后,我的常规动作是把它当契约源而不是参考资料,具体做三件事。第一件是目录对齐。按顶层用例图的三大块直接切包,把 16 条用例当成 Controller 里的方法名,一一对上号,谁没对上谁就是建模漏掉的:src/ modules/ auth/ # 用例 1、2:注册、登录 flight/ # 用例 3、11、12、13:查询与管理员维护 order/ # 用例 4、5、6、7、10:预订、查询、退票、打印 profile/ # 用例 8、9:个人信息查询与修改 stats/ # 用例 14:订票统计 shared/ state_machine.py # 第 4.4 节的迁移表第二件是把图从图片换成源码。用例图、类图、时序图全部改写成 PlantUML 放进docs/,和代码同仓提交,plantuml -tsvg挂到构建脚本里。这样做的实际收益是:改字段时 diff 看得见,评审时不用截图对图,半年后接手的人也不会拿到一张和代码完全对不上的图。第三件是拿状态机驱动代码。把第 4.4 节的迁移表写成 Python 字典,所有订单相关的写操作统一走一个校验函数,新增状态时只改这张表,业务代码不动:TRANSITIONS { (PENDING_PAY, pay): PAID, (PENDING_PAY, cancel): CANCELLED, (PAID, refund): REFUNDED, } def transit(current: str, action: str) - str: key (current, action) if key not in TRANSITIONS: raise BizError(f状态 {current} 不允许执行 {action}) return TRANSITIONS[key]验证方法也很朴素:每改一处,回到用例规约里找对应的那条,看前置条件和异常流还成不成立。我习惯用一张纸把 16 条用例打出来,做完一条划掉一条,划不掉的说明实现和建模已经分叉了。这套流程救过我至少两次,一次是发现打印票据漏了未预订要提示的异常流,一次是发现管理员统计接口没做二次验证。从那以后我每次接这种以建模文档为交付物的活儿,都强制自己先把状态迁移表和路由对照表写出来再动代码——图可以画得好看,表对不上就是白画。希望帮到你。本文还有配套的精品资源点击获取