简介:这份电影院售票管理系统UML设计文档,基于统一建模语言,完整呈现了电影院售票系统的分析与设计流程,适合软件工程学习者、系统分析与设计人员及UML初学者参考。文档从业务需求出发,明确了收入提升20%、员工有效工作时间增加20分钟等目标,并分析了业务风险与假设依赖。围绕顾客、员工、系统管理员三类参与者,文档详细描述了订票、变更订单、取消订单、查看订单、处理订单、检票、引进新片、更新数据库等12个用例,其中订票用例(UC-1)涵盖从顾客登录、选片、选座到支付确认的全过程,并提供了分支流程与异常处理方案。此外,文档还包含软件需求规格说明中的产品远景规划、用户类与用户特性描述,使读者能清晰理解需求到UML模型的映射。资源为单个PDF文件(650KB),结构完整、层次分明。已有4698人学习下载,适合作为课程设计、毕业设计或UML建模实践的参考素材。
1. 从“电影院售票管理系统UML.pdf”这个标题开始:一张图撑不起一个系统
看到这个标题,很多人的第一反应是找一份现成的《电影院售票管理系统——UML图》PDF去抄。真做过就会知道,单张UML用例图撑不起一个系统:影院有电影、场次、座位、票价,有人工窗口和自助终端,顾客和售票员在同一张用例图上互相拉扯;换一个放映厅,座位排布和退票规则又会变。这份文档真正该回答的是“需求的边界在哪、类的模型怎样对应数据库、一次购票的数据怎么流转”。这里从用例图开始,经过类图、UML包图到顺序图,最后落到用Visio画UML类图并把整组模型导出成PDF。读者不必有建模经验,最好懂一点关系型数据库和接口调用,否则画图时会卡在“这条线该画成组合还是聚合”上。
2. 电影院售票系统用例图:先把参与者和业务边界钉死
UML用例图是整个模型里最好上手也最容易画废的一张图。它的作用不是在图上堆满小人,而是把业务问题翻译成参与者和用例的边界。买票、退票、取票,谁发起、谁执行、谁被通知,都必须在这张图上提前商量清楚。很多团队跳过用例图直接画类图,结果开发到一半才发现“售票员不能给会员打折”这种需求没人提出来。
2.1 参与者:不只是顾客,还有售票员、管理员和支付网关
电影院售票管理系统的参与者可以从两个维度识别:人工发起方和自动触发方。人工发起方至少包括三类。顾客是线上或自助终端买票的用户;售票员是影院窗口的员工,他的购票流程比顾客多一步验票和收款,但不应该重新做一套“购票”用例;管理员负责排片、定价和场次管理。自动触发方是第三方支付网关,它不主动发起操作,但支付结果的确认依赖回调接口,若不在用例图中标出,后续类图设计容易漏掉异步回调消息。
下表是常用参与者清单,供画图时核对,不用原样照抄。
| 参与者 | 一句话职责 | 典型入口 |
|---|---|---|
| 顾客 | 查询场次、购买、取票、退票 | 小程序、自助终端 |
| 售票员 | 代客购票、现金收款、退票核验 | 窗口收银台 |
| 管理员 | 维护电影、场次、影厅、票价策略 | 后台管理页面 |
| 支付网关 | 处理支付、返回支付结果 | 异步回调接口 |
注意,售票员代客购票和顾客在线购票是同一个“购票”用例的两种具体化,不要拆成两个椭圆。如果拆成“线上购票”和“窗口购票”,后续类图中的订单来源字段就会分裂,界面层也要做两套逻辑。系统自动发促销短信不算参与者,那只是排片管理或会员营销的后置条件。
2.2 购票主流程的用例图骨架:include 和 extend 在哪条边
用例图的骨架是边界矩形、参与者和用例椭圆。边界矩形代表“系统对外承诺的内部功能范围”,所有要在这套系统内部实现的行为都放进矩形,不相关的比如“打扫影厅”不要画进来。以购票为例,选择场次、锁定座位、发起支付不是一个完整用例,而是购票用例中包含的必要步骤。支付成功后的取票是可选扩展,不取票也能刷二维码入场,所以用extend表达。
下面的PlantUML片段可以直接导入工具生成图,用于校验关系方向。
@startuml left to right direction skinparam packageStyle rectangle actor 顾客 actor 售票员 actor 管理员 rectangle "电影院售票管理系统" { usecase "购票" as UC1 usecase "选座" as UC2 usecase "支付" as UC3 usecase "取票" as UC4 usecase "退票" as UC5 usecase "排片管理" as UC6 } 顾客 --> UC1 UC1 ..> UC2 : <<include>> UC1 ..> UC3 : <<include>> UC4 ..> UC3 : <<extend>> 售票员 --> UC5 管理员 --> UC6 @enduml这组关系里,顾客发起的购票用例必然包含选座和支付,所以用带箭头的虚线指向被包含用例。箭头从基础用例指向被包含用例,方向不能反。取票是在支付完成后才可能出现的行为,也可能“不取票直接刷码入场”,因此用extend从取票指向支付,表示扩展点在支付完成后触发。管理员与排片管理之间没有include也没有extend,是独立后台行为。易犯的错误是到处使用extend表达依赖,那只会让图失去判断力。
2.3 用例描述三栏表:前置条件、主路径和异常流
一张UML用例图能表达的语义有限,真正的需求约定需要为每个关键用例补充三栏式描述。以“购票”为例,前置条件写“用户已登录,且存在上映场次”;主路径写五个不超过五行的步骤;异常流单独列“座位锁定超时”“支付超时”“余票不足”。不要试图在主路径里把所有if-else都写出来,否则阅读者无法判断这是需求文档还是伪代码。
| 字段 | 内容示例 |
|---|---|
| 用例名 | 购票 |
| 前置条件 | 用户已登录,选择场次处于售票期 |
| 主路径 | 1. 选择场次;2. 选择未售座位;3. 系统锁定座位10分钟;4. 支付成功;5. 生成电子票 |
| 异常流 | 3a. 座位被抢占,提示重新选座;4a. 支付超时,释放已锁座位 |
用例描述的主路径最后要被顺序图一页一页验证,因此不要出现“操作成功”“操作失败”这种没有内容的词。成功是什么结果,失败释放哪些资源,要能落到下一步模型。
2.4 用例命名的颗粒度参数怎么调
用例名用“动词+宾语”,比如“退票”“排片管理”。要避免“系统购票功能”这种名词混合写法,那会把用例当成菜单按钮。颗粒度则看这个用例能否独立回答“给用户带来什么业务结果”。帮助用户查时间表的“查询电影排期”和系统内部的“锁定座位”相比,“锁定座位”通常更适合作为购票用例的步骤而不是独立用例。只有当“锁定座位”存在独立的运营方需求,比如会员提前占座,才值得升级为独立用例。参数严格讲不是数学系数,而是评审时判断同一层用例粒度的两个问题:去掉这个用例,主流程还能不能走通;这个用例能否独立交付和测试。两个问题答案都是是,才把用例当作一级用例,否则降为步骤。
3. UML类图是模型的核心:把电影、场次、座位和票变成可落地的对象
用例图把行为边界划好之后,就要回答“这些行为靠哪些数据来完成”。UML类图在电影院售票管理系统中承担领域模型和数据库表的双重投影。它画错了,最直接的影响是售票时同一座位被两张订单锁住,或者退票后座位状态无法恢复。类图的每个属性、关联和多重性都要经得起数据库约束的反问。
3.1 从用例涉及的名词筛出实体与值对象
拿着第2章的用例清单逐列扫描,“顾客”“电影”“场次”“座位”“票”是实体,“支付金额”“支付时间”是依附于订单的值对象。区分实体和值对象的标准是:有没有独立标识、会不会变化后导致身份改变。电影有电影ID,哪怕名字改变,还是同一部电影;“10号厅第3排”的座位号本身是值对象,但还需要Seat对象表明它在哪个影厅且是否售出。
下面是第一版核心类图的PlantUML表达,可以直接改关系角标再运行。
@startuml class Movie { -id : Long -title : String -duration : Integer -language : String -releaseDate : Date } class Session { -id : Long -startTime : DateTime -endTime : DateTime -basePrice : BigDecimal } class Hall { -id : Long -name : String -rows : Integer -cols : Integer } class Seat { -id : Long -rowNo : Integer -colNo : Integer -seatType : String } class Ticket { -id : Long -code : String -price : BigDecimal -status : TicketStatus } Movie "1" -- "1..*" Session Hall "1" -- "1..*" Session Hall "1" -- "1..*" Seat Session "1" -- "1..*" Ticket Ticket "0..1" -- "1" Seat @enduml这个图有两个关键参数。第一个是多重性:“1”表示一个电影能对应多个场次,一个场次只能属于一个电影;Hall与Session、Seat的关系同理。Session和Ticket是一对多,因为一个场次可以卖很多票。Ticket和Seat之间是多对一:一张票引用一个座位,一个物理座位在不同场次会被多张票引用。注意这里Seat没有直接挂“是否空闲”属性,空闲状态应该由“场次+座位”的组合决定,这一点在后文会专门说明。
3.2 关联方向:从“能互相访问”改成“必须明确由谁持有谁”
类图上最常见的问题是在两个类之间画一条实线,但两端不标明导航方向。在售票系统里,Ticket持有Session的ID和Seat的ID,而不是Session持有所有Ticket集合,因为查询“一场卖出多少张票”需要用session_id去TicketRepository过滤。建议在类图中把依赖方向画成“接口或服务依赖领域类,领域类之间不要双向依赖”。一旦出现Session和Ticket互相引用,就容易在一次事务里改两边数据,造成锁冲突。
聚合和组合的判定值得单独说。Ticket与Session之间,如果场次被下架,已售出的票仍然留在用户手里,只是不能再进场,这种生命周期不完全由父类控制的关联用聚合;Hall与Seat是组合,因为影厅不存在了,座位作为物理座位标识也失去归属。不要把“删除父对象时子对象跟着删除”当作设计目的来画组合,要回到业务生命周期:座位可以单独建,票离开场次后仍可追溯,所以Ticket对Session更接近聚合。类图中实心菱形还是空心菱形,直接影响代码里的级联删除策略。
3.3 用状态枚举约束非法流转:座位状态和票状态
座位状态不能写成Seat上的一个布尔值isAvailable,因为同一座位在不同场次下状态不同。正确做法是定义SessionSeat或Ticket状态作为锁定记录。以下代码用枚举说明合法状态集合:
@startuml enum SeatStatus { AVAILABLE LOCKED SOLD } enum TicketStatus { CREATED PAYING PAID CANCELLED REFUNDED } Ticket *-- TicketStatus SessionSeat *-- SeatStatus @enduml这段代码表达的是约束,不是建议在数据库里多加一张表。实际建表时,Ticket表里的status字段可以用字符串枚举,并在代码层禁止从CREATED直接跳到REFUNDED。售票系统里最容易混淆的非法流转有三种:已支付票被反复退、已取消票再次入账为成功、AVAILABLE座位被两个流程同时改成LOCKED。处理办法是把枚举流转表写进类图注释,再让开发在事务里加行级锁或乐观锁版本号字段。
下面是最小合法流转表:
| 当前状态 | 允许动作 | 目标状态 |
|---|---|---|
| AVAILABLE | 选择并锁定 | LOCKED |
| LOCKED | 支付 | SOLD |
| LOCKED | 超时释放 | AVAILABLE |
| SOLD | 退票 | REFUNDED |
由此能看出,类图不是静态数据库表快照,它要把约束规则告诉后续代码和测试。参数上可以给Ticket和SessionSeat增加乐观锁version字段,避免高并发下状态互相覆盖。
3.4 把唯一性约束标进类图注释
电影院卖票有两个容易挖坑的唯一性问题:同一场次内座位不能被重复销售,同一支付流水不能对应两张票。前者用(session_id, seat_id)联合唯一索引解决,后者需要在payment_transaction表上做外部唯一键。类图可以在Seat和Ticket之间画一条注释线,写“{ session_id, seat_id 唯一 }”。不要只写在数据库迁移文件里,因为阅读PDF的需求方不一定看得懂迁移脚本。标注格式可以是一行文本,放在类图空白角落并画虚线连接Ticket类。后续生成代码时,这会直接对应到唯一索引定义。
还要注意,类图中的数据类型不能太随意。金额用BigDecimal而不是double,时间用DateTime而不是String。如果类图里价格写成double,交付给后端时大概率要返工。
4. UML包图和顺序图配合使用:让售票流程在模型里演一遍
类图把静态关系定下来了,但一次购票由谁创建、先锁座位还是先建订单,仍然是悬而未决的动态问题。UML包图把类按照依赖和职责装进容器,顺序图则把关键流程走一遍。两张图合在一起,能暴露前面设计的缺口。常见结果是:类图画得头头是道,演进到包图时才发现领域类被Controller直接改属性,顺序图里出现跨层调用仓库。
4.1 UML包图划分:依赖优先于按技术栈拉条块
不要按Controller、Service、Dao这种技术词来分包,那只是分层,没有表达领域边界。电影院售票系统的包图至少要有四个包:表示层、应用层、领域层、基础设施层。领域包里放Movie、Session、Hall、Seat、Ticket及其状态枚举;应用包里放BookingService、PaymentService等编排服务;基础设施包放Repository和数据库映射;表示层放Web接口。包与包之间只能沿着表示层到应用层再到领域层的方向依赖,基础设施包可以依赖领域层接口,但不要反向。
下面是一份最小包图代码:
@startuml package "表示层" { [TicketController] [SeatController] } package "应用层" { [BookingService] [PaymentService] } package "领域层" { [Ticket] [Session] [Seat] } package "基础设施层" { [TicketRepository] [SessionMapper] } TicketController --> BookingService SeatController --> BookingService BookingService --> Ticket BookingService --> Session BookingService --> Seat BookingService --> PaymentService PaymentService --> Ticket TicketRepository --> Ticket @enduml这里没有把领域层指向基础设施层,说明领域层接口由基础设施层实现。改进方向是在TicketRepository旁加一个小图标表示接口,再让基础设施层实现类指向它。UML包图的检查参数只有两个:是否存在循环依赖,领域层是否被表示层直接访问。出现任何“TicketController直接new Ticket”的依赖线,就把这条线标红重画。
4.2 顺序图:购票流程的完整消息和时间顺序
顺序图的对象不应该是数据库表,而应是“页面、服务、领域对象、外部网关”这些能执行动作的对象。以购票主路径为例,消息顺序为:顾客提交选择,购票页面调用BookingService.book,BookingService先执行SeatService.lockSeat,锁定成功后发起支付,支付回调后更新票状态。顺序图能让“支付时间点到底在锁座后还是创建订单后”这个问题在评审桌上吵清楚。
@startuml actor 顾客 participant "购票页面" as Page participant "BookingService" as Booking participant "SeatService" as SeatService participant "PaymentClient" as Payment 顾客 -> Page : 选择场次与座位 Page -> Booking : book(sessionId, seatId) Booking -> SeatService : lockSeat(sessionId, seatId) SeatService --> Booking : SeatLockResult(true) Booking -> Payment : createPayment(orderNo, amount) Payment --> Booking : paymentId Booking --> Page : 待支付 顾客 -> Payment : 调用支付 Payment -> Booking : 支付回调(paymentId) Booking -> SeatService : confirmSeat(sessionId, seatId) Booking -> Page : 支付成功 @enduml当交互里出现支付回调,要在对象Payment旁边增加一条生命线,不能只用一条“支付”消息覆盖超时。返回消息的意义是记下上一动作的输出,而不是模拟一次HTTP响应,它在代码注释中通常等于方法返回值。顺序图和用例描述对账时,主路径5个步骤必须以垂直方向扫过每条生命线:顾客、页面、服务、外部网关依次出现。如果发现某个步骤靠用户去数据库里改数据,说明遗漏了服务方法。
4.3 异常分支用alt与opt表达,不要在消息名里写“不成功”
顺序图的经营经常忽略异常处理,导致项目组以为一切顺利。规范做法是用alt片段把“座位已被别人锁住”和“支付超时”单列成组合片段。具体写法如下:
@startuml actor 顾客 participant 购票页面 participant BookingService participant SeatService 顾客 -> 购票页面 : 选座并提交 购票页面 -> BookingService : book(sessionId, seatId) BookingService -> SeatService : lockSeat(sessionId, seatId) alt 锁定成功 SeatService --> BookingService : LOCKED BookingService -> BookingService : 生成待支付订单 else 座位不可用 SeatService --> BookingService : SEAT_UNAVAILABLE BookingService --> 购票页面 : 提示重新选座 end @enduml在顺序图里,一条消息名只能描述动作,不要写“如果失败怎么办”。参数上,要给lockSeat(sessionId, seatId)加上一个锁定时限,比如“锁定10分钟”,再在图中用注释标出。这样模型做完时,开发到锁座不会再问“超时多久释放座位”。与此同时,不要把所有异常分支都画出来,否则图会失去可读性;只画改变主流程走向的关键分支。
5. 用Visio画UML类图的三处参数设置,以及导出PDF时的检查点
既然目标文件是“电影院售票管理系统UML.pdf”,通常交付场景是从Visio导出完整模型。用Visio画UML类图这件事,难点不是画矩形,而是画完后类之间的关系线、属性显示和导出缩放都失控。这里给出我常用的处理方式。
5.1 打开Visio的UML静态结构模板,别用基础流程图硬拼
在Visio 2013之后的版本中,从“文件-新建-类别-软件和数据库”选择“UML模型图”或“静态结构”。如果左侧形状面板找不到类、接口、聚合线,直接搜索“UML”,选择带类图符号的模板。这一步保证类形状可以双击添加属性,而不是一个普通矩形。打开模板后,新建一个类形状,在“属性”视图中添加字段和方法;属性名用字段名,方法签名写返回类型加方法名。
5.2 类形状的三处关键参数
右键类形状选择“形状显示选项”,需要确认三个位置。第一,属性区域是否勾选了“显示属性”,不勾选的话PDF里只能看到类名,看不到字段;第二,在“方法”区域勾选“显示方法”,并选择“按原样显示”或“仅显示签名”;第三,在“规范”设置里取消“按原型分组”,否则属性和操作会折叠进同一个分组,整张图像空白图纸。这三个参数在Visio 2013之后基本一致。
下表是默认推荐项:
| 显示项 | 推荐值 | 说明 |
|---|---|---|
| 属性区 | 显示“类型 名称” | 便于与Java字段对应 |
| 方法区 | 仅显示签名 | 可容纳参数类型 |
| 可见性标记 | 显示“+”“-” | 便于判断类边界 |
| 多重性 | 显示在关联线两端 | 不要省略 |
关联线的设置也需要处理。在“视图-视觉帮助”打开“自动连接”,鼠标从类形状边缘拖出关系线,Visio才会画自动贴附的关联线,而不是独立折线。如果线条文字重叠,调整文本块间距,不要在PDF里出现数字压在线上。
5.3 导出PDF前跑一遍可读性检查
第一,检查每张类图的右下角是否标注“UML 2.5”或所用建模规范版本。存档文档要有图语言版本,否则三年后没人知道这套符号含义。第二,检查多重性数字不是空斑:没有连接线但看起来像两条线交叉,容易让人误以为类之间有关联。第三,检查缩放比例:使用“文件-另存为-PDF”,在优化选项中选择“最小化文件大小”,然后在PDF阅读器里把页面放大到200%,看线条是否断线。Visio默认输出矢量图形,文本不会发虚,但若从文件菜单走“打印到PDF”,可能按打印分辨率输出,反而不清晰。常规做法是直接另存为PDF,不经过打印功能。
做完这三步,把类图、用例图、UML包图和顺序图按顺序组合成一个PDF,并确认每个图页文件名能对应图表标题。不要在一张PDF里塞满未命名标签页,评审时最常问的就是“某条蓝色箭头含义”,所以每一页图底部都写一句图例说明。最后检查“同一场次同一座位唯一”这条约束是否以注释形式出现在类图页面中。若没有,就把这个注释补上,因为它是整张模型中最容易被质疑的业务规则。
本文还有配套的精品资源,点击获取