news 2026/10/28 2:09:23

航空订票系统UML建模落地:用例规约、接口、建表与事务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
航空订票系统UML建模落地:用例规约、接口、建表与事务

简介:《航空订票系统 UML 建模设计》是一份面向软件工程课程设计、毕业设计及 UML 建模入门的文档型参考资料,围绕航空公司订票子系统的需求分析与模型表达展开,适合需要完成系统设计文档或学习 UML 多种视图的学生与开发者。压缩包内共 1 个 docx 文件,大小约 535KB,内容以文字与图形化建模说明为主,便于直接查阅和二次整理。文档从需求描述入手,依次给出用户、管理员与顶层用例图,并整理用户类、管理员类、航班信息类、机票类、个人信息类等类图关系,同时覆盖登录、查询、预订、退票等状态图与顺序图,以及用户和管理员协作的合作图。其中还包含用例规约、异常事件流与后置条件等细节,可帮助读者理解订票模块、航班信息管理模块和个人信息管理模块的职责划分,并对照完成自己的建模作业或项目文档。目前已有 1626 人学习,适合作为 UML 建模与系统设计阶段的参考样例。

1. 只有图没有代码的航空订票系统:UML 建模设计能落地到哪一步

一份只画了图、没写一行代码的航空订票系统 UML 建模设计,能拿来干什么?我第一次拿到这种 docx 时的反应是:图挺全,然后呢。翻完才发现它的价值不在图好不好看,而在于需求、用例规约、类图、时序图这四层是串起来的——需求里写死「航班未发出才能退票」,用例规约里就有退票的前置条件,时序图里就有对应的校验调用。这套航空订票系统的 UML 建模设计覆盖了游客、注册用户、管理员三类角色,16 条用例规约,五个核心类和多张交互图,适合课程设计阶段需要完整建模骨架的人,也适合想照着模型反推表结构和接口的后端。下面按建模顺序往下拆,顺手补上原文档没写、真动手时绕不开的实现细节。

2. 角色与权限边界:游客、注册用户、管理员的三层准入怎么写死

权限这件事,建模阶段画一条「参与者—用例」的连线就完事了,写代码时却是第一个出问题的地方。这份设计把角色切得比一般课程设计细:游客、注册用户、管理员,而且管理员还额外挂了一层「敏感操作二次验证」。这一章把这三层准入拆成能直接照着写的规则。

2.1 三类角色的准入规则与首页拦截

需求第一条把边界划得很干脆:游客只能停在首页,首页给两个入口——登录和注册。这条看着最简单,落地时最容易翻车的地方是只做了前端跳转,后端接口裸奔,改个 URL 就能进查询页。我的做法是前端路由守卫和后端接口鉴权各写一遍,两边用同一份角色表,改的时候只改一处。

// 路由守卫:role 来自登录后签发的会话,未登录一律视为 guest const ROUTES = { '/': ['guest', 'user', 'admin'], // 首页对所有人开放 '/login': ['guest'], '/register': ['guest'], '/flights': ['user', 'admin'], // 航班查询 '/orders': ['user'], // 我的订单、退票、打印 '/profile': ['user', 'admin'], // 个人信息查询与修改 '/admin/flights': ['admin'], // 航班信息增删改 '/admin/stats': ['admin'], // 订票数据统计 }; function guard(to, session) { const allow = ROUTES[to] || []; const role = session?.role ?? 'guest'; // 没有会话就是游客 if (!allow.includes(role)) { return role === 'guest' ? '/login' : '/'; // 游客踢回登录,越权踢回首页 } return null; // 放行 }

ROUTES的键是页面路径,值是这个页面允许的角色数组,新增页面时同步加一行即可;session里的role是登录成功时服务端签发的,不能由前端自己写。guard返回null表示放行,返回字符串表示重定向目标。要注意这套东西只解决「用户看到什么」,真正的权限判定必须在每个接口上再做一次,否则前端路由表就是一层纸。

注册用户的边界值得单独说一句:需求里写「用户可以查看自身所有预定的航班票」,这里的「自身」两个字是硬约束。订单查询接口必须从会话里取user_id拼进WHERE条件,而不是从前端传参里取。我见过太多把userId当查询参数传的实现,改个数字就能看别人的订单,这类问题在评审时几乎必被挑出来。

管理员这条线更绕一点。需求里明确写了「管理员用户的注册为特殊用户注册,由系统的最高权限的管理员分派标识符或者由航空公司内部员工号进行区别鉴定」。翻译成实现就是:管理员不能走普通注册入口自助注册,账号来源要么是初始化脚本,要么是超管在后台开号,role字段由服务端赋值,注册接口里绝对不能出现「让前端传 role」这种设计。

2.2 敏感操作的二次鉴权:一次登录不等于一直可信

原文档里有一段容易被略过但很关键的描述:管理员登录方式和普通用户一样,但做敏感操作时要再输一次标识符验证。这不是多此一举,而是把「登录态」和「操作授权」拆成了两层。登录只证明你是谁,敏感操作要再证明一次你现在还在这儿。

常见做法是发一个短时效的二次凭证。管理员点击「添加航班」时先弹窗输入标识符,服务端校验通过后签发一个 TTL 五分钟的op_token,前端把它放在请求头里,后续的增删改接口都校验这个头。

# 敏感操作二次鉴权:登录态之外再加一层短时效凭证 import time, hmac, hashlib OP_TTL = 300 # 二次凭证有效期 5 分钟 def issue_op_token(user_id: int, identifier: str, secret: bytes) -> str | None: """校验管理员标识符,通过则签发二次凭证;失败返回 None""" admin = db.query_one( "SELECT id, staff_no, identifier_hash FROM t_user " "WHERE id = %s AND role = 1", user_id) if admin is None: return None # 标识符只存哈希,比对时用常量时间比较,避免时序侧信道 digest = hashlib.sha256(identifier.encode()).hexdigest() if not hmac.compare_digest(digest, admin["identifier_hash"]): return None expire = int(time.time()) + OP_TTL raw = f"{user_id}:{expire}" sign = hmac.new(secret, raw.encode(), hashlib.sha256).hexdigest()[:16] return f"{raw}:{sign}" # 形如 1001:1735689600:ab12cd34ef56gh78 def check_op_token(token: str, secret: bytes) -> bool: try: user_id, expire, sign = token.split(":") except ValueError: return False # 格式不对直接拒绝 if int(expire) < time.time(): return False # 过期,重新输标识符 raw = f"{user_id}:{expire}" expect = hmac.new(secret, raw.encode(), hashlib.sha256).hexdigest()[:16] return hmac.compare_digest(sign, expect)

OP_TTL是按「一次操作一分钟内做完」的节奏定的,太长等于没做二次验证,太短会让管理员频繁重输。identifier_hash用 SHA-256 存,别存明文,也别用可逆加密。secret放环境变量,不要进仓库。这套逻辑用无状态签名就够了,不一定要上 Redis;如果要做「用一次就失效」,那就得存服务端并标记已消费。

真正容易漏的是覆盖面:哪些接口算敏感操作?按需求,添加、修改、删除航班信息都算,统计订票数据算不算可以商榷,我一般按「会改动数据」来划,只读接口不加这层,否则管理员用起来太累。

2.3 把需求条目翻译成能测的验收点

需求原文是六条自然语言,直接照着写代码会漏分支。我在动手前会先把每一条拆成「正常路径 + 至少两个失败路径」,这份表就是后面写用例规约和接口测试的底稿。

需求原文可测的验收点必须覆盖的失败分支
游客只能访问首页未登录访问 /flights 跳转 /login直接请求查询接口返回 401
登录后进入查询页正确账号密码返回会话与角色用户名不存在、密码错误、账号被禁用
查询到航班才能订票有结果时可下单,生成待支付订单无结果、余票为 0、航班已起飞
未发出航班可退票可付款起飞前退票成功、付款后状态变更起飞后退票、重复退票、非本人订单
管理员增删改航班三类操作各自成功并落库字段缺失、格式非法、无二次凭证
身份区分验证普通用户访问管理接口被拒管理员凭证过期、越权访问他人订单

这张表的价值在于,它把「用户可以退票」这种模糊表述逼成了「起飞前 + 本人订单 + 状态为已支付或已出票」三个条件同时成立。后面写用例规约的异常流、写接口的参数校验,都是从这里往下推的。我一般会把这张表直接贴进项目 README,改需求时先改表,再改代码,省得半年后没人记得当初为什么加这个判断。

3. 用例图与用例规约:16 条规约怎么变成一张接口清单

用例图是最容易画成「功能清单」的东西——一个大圈套一堆小圈,看着热闹,实际信息量接近零。这份建模设计好在把用例图按参与者切成了三张,又给每条用例配了规约,这就有了往下推接口的基础。这一章讲怎么读这三张图,以及怎么把规约里的基本流和异常流变成接口的参数校验。

3.1 三张用例图为什么按参与者切而不是按模块切

原文档的做法是:顶层用例图放三个大用例(个人信息管理、订票管理、航班信息管理),参与者是注册用户和系统管理员;然后分别画用户用例图和管理员用例图,把大用例展开成具体动作。用户侧展开出注册、登录、航班查询、机票预订、已定机票查询、退票、修改已订航班个人信息、查看个人信息、修改个人信息、打印票据十个;管理员侧展开出注册、登录、添加航班信息、修改航班信息、删除航班信息、查看个人信息、修改个人信息、统计订票数据八个。

按参与者切的好处是权限边界在图上一目了然:凡是出现在两张图里的用例(登录、查看个人信息、修改个人信息),就说明它是共享能力,实现时应该抽成公共服务,而不是在用户控制器和管理员控制器里各写一遍。凡是只出现在一张图里的,就是角色专属,可以在路由层直接隔离。

坏处是粒度容易失控。「订票管理」这个大用例在顶层图里是一个圈,展开之后变成预订、查询、退票好几个动作,如果每个动作再往下拆成「点击预订按钮」「确认订单」,图就没法看了。我的判断标准是:一个用例对应一次「参与者发起、系统给出可观察结果」的完整交互,中间步骤不进用例图,进时序图。

还有一个常被忽略的点:原文档把「注册」同时给了注册用户和管理员两个参与者。按需求原文,管理员注册是特殊注册,不开放自助入口,所以这张图在实现层面应该理解成「管理员账号创建」,而不是复用前台的注册接口。图上画成一个用例没问题,代码里必须是两条路径,这是后面容易埋坑的地方。

3.2 用例规约的模板与它的缺口

每条规约的骨架是固定的:用例名称、用例 ID、角色、用例说明、前置条件、基本事件流、其他事件流、异常事件流、后置条件。这套模板的好处是把「谁在什么时候能做什么」写死了,坏处是写的人容易偷懒。

拿规约 ID 4「机票预订」举例,基本事件流写的是「用户点击预订按钮 → 系统获取预订机票信息与用户信息进行订票」,异常事件流直接写「无」。这就是典型的糊弄。真实的预订至少有三类失败:余票不足、同一乘客在同一航班重复下单、航班已经起飞。这三条不写进规约,写代码的人就会默认「预订一定成功」,等上线后接到第一个超卖投诉才开始补。

我在补这套规约时,会给每条用例硬性加两个字段:一是「业务前置校验」,明确列出进入基本流之前必须成立的断言;二是「失败返回」,列出每种失败对应的错误码和用户可见提示。以预订为例,补完之后是这样:

项内容
前置校验会话有效且角色为注册用户;航班存在且未起飞;余票大于 0;同一证件在本航班无有效订单
基本流提交乘机人信息 → 校验 → 扣减余票 → 生成待支付订单 → 返回订单号
失败返回40001 余票不足 / 40002 重复下单 / 40003 航班已起飞 / 40004 乘机人信息不完整
后置条件订单状态为待支付,余票减一,两者在同一事务内

规约 ID 6「退票」的缺口更明显:原文只写了「系统根据订票信息 id 进行退票操作」,前置条件只有「用户进入查询页面」。但需求第 3 条白纸黑字写着「在一定条件下(即航班未发出)可以进行退票」。这个条件必须在规约里体现成前置校验,否则实现时很容易只判断「订单存在」就退了。

规约 ID 1「用户注册」的异常流写得倒是相对完整:未输入用户名、未输入密码、两次密码不同,各对应一条提示。这里可以再补两条:用户名已存在、密码强度不达标。后置条件写的是「注册成功,跳转登录界面」,说明注册不自动登录,这条对前端流程有直接影响,别自己改成注册即登录。

3.3 从规约推出接口清单和参数约束

16 条规约摆在那儿,下一步是把它们映射成 HTTP 接口。映射规则不复杂:一条操作用例对应一个接口,查询类用 GET,变更类用 POST/PUT/DELETE,角色决定权限,前置校验决定参数必填项。我一般会先出一张映射表,评审过了再动手写代码。

用例 ID接口方法与路径角色关键参数
1用户注册POST /api/register游客username, password, confirmPassword
2登录POST /api/login游客username, password
3航班查询GET /api/flights注册用户from, to, date
4机票预订POST /api/orders注册用户flightId, passenger, idCard
5已定机票查询GET /api/orders注册用户无(user_id 取自会话)
6退票POST /api/orders/{id}/refund注册用户orderId
7修改已订信息PUT /api/orders/{id}/passenger注册用户passenger, phone
8/9个人信息查改GET/PUT /api/profile注册用户、管理员realName, phone
10打印票据GET /api/orders/{id}/ticket注册用户orderId
11~13航班增删改POST/PUT/DELETE /api/admin/flights管理员需 op_token 请求头
14统计订票数据GET /api/admin/stats管理员startDate, endDate

这张表里有两个细节值得说。一是用例 5 的「已定机票查询」,参数列写「无」,因为user_id必须从会话里取,这是需求里「查看自身所有预定的航班票」的直接落地;如果这里放开了userId参数,就等着出越权漏洞。二是用例 10 打印票据,接口返回的应该是可渲染的票据数据而不是 PDF 二进制——生成 PDF 属于表现层的事,放服务端做会让这个接口变得又重又难测,常见做法是返回 JSON,前端用打印样式表直接调浏览器打印。

参数约束建议直接写进接口定义文件,让校验和文档同源。下面是预订接口的一小段 OpenAPI 描述,字段的pattern和maxLength都是从用例规约的前置校验里抄下来的:

# openapi 片段:预订接口的参数约束,直接对应用例规约 ID 4 的前置校验 /api/orders: post: summary: 机票预订 security: [{ session: [] }] requestBody: required: true content: application/json: schema: type: object required: [flightId, passenger, idCard] properties: flightId: type: integer minimum: 1 passenger: type: string maxLength: 32 # 与证件姓名长度对齐 idCard: type: string pattern: '^\d{17}[\dXx]$' # 18 位身份证,末位可能是 X phone: type: string pattern: '^1\d{10}$' responses: '200': { description: 返回 orderNo 与待支付状态 } '400': { description: 参数校验失败,返回 40004 } '409': { description: 余票不足或重复下单,返回 40001/40002 }

这份定义的好处是,接口文档、参数校验、前端表单规则可以都从它生成,改一处三处同步。团队里如果没有这套工具链,退一步也要把required和pattern手抄进后端校验逻辑,别指望前端拦得住。

4. 类图与时序图对齐:五个核心类到建表语句和订票事务

静态图画得再漂亮,落到数据库就是几张表加几个外键;动态图画得再细,落到代码就是几个方法调用加一个事务边界。这一章把类图和时序图这两层对齐着看,重点讲清楚「图上的一条线」在代码里对应什么。

4.1 五个核心类与它们之间的关系

原文档列的核心类是用户类、管理员类、航班信息类、机票类、个人信息类。这个划分基本合理,但有两处需要在实现时修正。

第一处是管理员类和用户类的关系。图上画成两个独立的类,实际实现里我倾向于让管理员继承用户,或者干脆合成一张表用role字段区分。理由是登录、个人信息查询、个人信息修改这三条用例两边都有,合成一张表能省掉大量重复代码;管理员专属的staff_no和identifier_hash作为可空字段挂在同一张表上,普通用户为 NULL。用继承的话,ORM 层要么做单表继承要么做 joined 继承,对这么小的系统来说都是过度设计。

第二处是「机票类」和订单的关系。图上把机票和订票行为混在一个类里,实际实现必须拆开:Flight是航班,有航班号、起降时间、价格、余票;Order是订单,有订单号、乘机人、证件号、金额、状态。一个航班对应多个订单,一个用户对应多个订单,订单是这两者的关联实体,而不是航班的属性。这个拆分不做,后面余票扣减和退票回补都没地方放。

关系上还有一点容易画错:订单和乘机人信息是组合还是聚合?我的判断是组合——订单删除,乘机人信息跟着没意义;但反过来说,用户修改个人信息不应该影响已生成的订单,因为订单里的乘机人是下单时的快照。这个「快照」的语义在图上看不出来,但它直接决定了 4.2 的建表方式:订单表要把乘机人姓名和证件号冗余存一份,而不是只存一个passenger_id外键。

4.2 类图落到建表语句

按上面修正后的模型,核心表就三张:用户、航班、订单,个人信息并进用户表。下面两张是最关键的。

-- 用户表:注册用户与管理员共用,role 区分;管理员专属字段可空 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password_hash CHAR(60) NOT NULL, -- bcrypt 固定 60 位 role TINYINT NOT NULL DEFAULT 0,-- 0 注册用户 1 管理员 staff_no VARCHAR(32) DEFAULT NULL, -- 管理员内部员工号 identifier_hash CHAR(64) DEFAULT NULL, -- 敏感操作标识符的 SHA-256 real_name VARCHAR(32) DEFAULT NULL, id_card VARCHAR(18) DEFAULT NULL, phone VARCHAR(11) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username), UNIQUE KEY uk_staff_no (staff_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

password_hash用CHAR(60)是因为 bcrypt 输出定长,用VARCHAR(255)属于随手写。staff_no加唯一索引是为了让「内部员工号鉴定身份」这条需求可执行——两个人拿到同一个员工号是不可能的。identifier_hash允许为空,普通用户不填。

-- 订单表:乘机人信息存快照,不引外键,避免用户改资料影响历史订单 CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no CHAR(20) NOT NULL, user_id BIGINT NOT NULL, flight_id BIGINT NOT NULL, passenger VARCHAR(32) NOT NULL, -- 下单时的乘机人姓名快照 id_card VARCHAR(18) NOT NULL, -- 下单时的证件号快照 phone VARCHAR(11) DEFAULT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0 待支付 1 已支付 2 已出票 3 已退票 4 已取消 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME DEFAULT NULL, UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status), KEY idx_flight (flight_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

status用 TINYINT 而不是字符串,是为了让索引短、比较快,代价是可读性差,所以注释必须写全,最好在代码里配一个枚举类,别在业务逻辑里散落status == 2这种魔法数字。idx_user_status这个联合索引是给「我的订单」页面用的,查询条件固定是user_id = ? AND status IN (...),走这个索引刚好。

余票字段放在航班表上,seats_left INT NOT NULL,扣减时必须用UPDATE ... SET seats_left = seats_left - 1 WHERE id = ? AND seats_left > 0,靠影响行数判断是否成功,而不是先查再改。这个细节在类图上看不出来,但它决定了并发下会不会超卖。

这里有个我踩过的坑值得提前说:很多人会给订单表加UNIQUE KEY (flight_id, id_card)来防重复下单。问题是退票之后这个唯一键仍然占位,同一个乘客想重新订同一航班就插不进去了。要么把status也拼进唯一键(但退票后状态变了还是冲突),要么在退票时把id_card挪到一个archived_id_card字段。我一般选后者:唯一键保持(flight_id, id_card),退票时把id_card置空、原值写进归档字段,这样既能防重又不挡复订。

4.3 登录时序图:对象职责怎么分配到方法

原文档的登录时序图画的是「用户输入 → 界面 → 系统验证 → 响应」,这条链在代码里至少对应四层:控制器、服务、仓储、密码校验器。图上的「系统」是一个黑匣子,拆开之后每层该干什么得自己定。

我的分工是:控制器只做参数接收和响应封装,不碰业务;服务层负责「查用户、验密码、签会话」三步;仓储层只做 SQL;密码校验单独抽一个函数,方便以后换算法。登录失败时统一返回「用户名或密码错误」,不区分是哪一个错了——原文档的异常流里分开提示「未输入密码」和「未输入用户名」是可以的(那是空值校验),但账号不存在和密码错误必须合并提示,否则等于给撞库的人送情报。

会话签发之后,user_id和role一起写进去。管理员登录成功后,前端要额外判断role === 1才显示管理入口,但这只是体验优化,真正的拦截在后端。

4.4 订票到退票:时序图的终点是一个事务和一台状态机

原文档画了查票与订票的协作图、退票的时序图,但都没画事务边界。订票这条链在实现上必须是一个事务:扣余票、写订单、返回订单号,三步要么全成要么全不成。中间任何一步失败,余票都要回滚,否则就是「扣了座没生成订单」这种查不出来的脏数据。

def book(user_id: int, flight_id: int, passenger: str, id_card: str) -> str: """订票:行锁 + 余票扣减 + 订单写入,同一事务内完成""" with db.transaction(): # 事务边界从这里开始 flight = db.query_one( "SELECT id, price, seats_left, depart_time FROM t_flight " "WHERE id = %s FOR UPDATE", (flight_id,)) # 行锁,防并发超卖 if flight is None: raise BizError(40401, "航班不存在") if flight["depart_time"] <= now(): raise BizError(40003, "航班已起飞,无法预订") if flight["seats_left"] <= 0: raise BizError(40001, "余票不足") # 锁内再判,避免乐观判断失效 affected = db.execute( "UPDATE t_flight SET seats_left = seats_left - 1 " "WHERE id = %s AND seats_left > 0", (flight_id,)) if affected == 0: raise BizError(40001, "余票不足") # 双保险 order_no = gen_order_no() # 业务单号,与主键分离 db.execute( "INSERT INTO t_order(order_no, user_id, flight_id, passenger, " "id_card, amount, status) VALUES (%s,%s,%s,%s,%s,%s,0)", (order_no, user_id, flight_id, passenger, id_card, flight["price"])) return order_no # 事务提交后才返回

FOR UPDATE是这里的关键,没有它,两个请求同时读到seats_left = 1就会都往下走。amount取的是下单时刻的航班价格,不是实时价格,这也是一种快照——航班调价不应该影响已经生成的订单。order_no和主键分开是因为主键自增会泄露单量,也不方便对外展示,单号一般用「日期 + 随机 + 校验位」拼。

退票那边,原文档的时序图画了「点击退票 → 系统根据订票信息 id 退票」,但需求里的「航班未发出」这个条件没体现。实现上我会把订单状态迁移独立成一台小状态机,所有状态变更都必须过它。

# 订单状态机:只允许表里列出的迁移,其余一律拒绝 TRANSITIONS = { 0: [4], # 待支付 -> 已取消 1: [3], # 已支付 -> 已退票 2: [3], # 已出票 -> 已退票 3: [], # 已退票,终态 4: [], # 已取消,终态 } def refund(order_id: int, user_id: int) -> None: with db.transaction(): order = db.query_one( "SELECT id, flight_id, status, id_card FROM t_order " "WHERE id = %s AND user_id = %s FOR UPDATE", (order_id, user_id)) if order is None: raise BizError(40301, "订单不存在或不属于当前用户") flight = db.query_one("SELECT depart_time FROM t_flight WHERE id = %s", (order["flight_id"],)) if flight["depart_time"] <= now(): raise BizError(40005, "航班已发出,不可退票") # 需求里写死的前置条件 if 3 not in TRANSITIONS.get(order["status"], []): raise BizError(40006, "当前状态不可退票") db.execute("UPDATE t_order SET status = 3, id_card = NULL, " "archived_id_card = %s WHERE id = %s", (order["id_card"], order_id)) db.execute("UPDATE t_flight SET seats_left = seats_left + 1 WHERE id = %s", (order["flight_id"],)) # 回补余票 write_refund_log(order_id, user_id) # 退款流水,便于对账

TRANSITIONS这张表看着多余,实则是后悔药:以后业务加了「已出票不可退、只能改签」的规则,只改这张表,不用去翻散落各处的if。user_id拼进WHERE是防越权退别人的票。archived_id_card是 4.2 提到的防重方案配套字段,退票时把证件号挪过去,唯一键自然释放。

5. 避坑与常见问题:从模型到代码之间最常翻车的 5 个地方

建模文档看起来四平八稳,真动手时该踩的坑一个不少。下面五条是我照着这类模型落地时反复遇到的,按「现象 → 原因 → 解决」写,能省一次返工就值了。

5.1 建模阶段的三个坑

用例粒度失控,一张图塞进二十个圈。现象是用户用例图里出现「点击预订按钮」「输入查询条件」这种动作级用例,图越画越大,评审时没人看得下去。原因是画图的人把「操作步骤」当成了「用例」。解决方法是回到参与者的目标:一个用例应该是参与者为了达成某个目的、系统给出一份可观察结果的一次完整交互,「预订机票」是, 「点击预订按钮」不是。中间步骤交给时序图。

类图里没有关联类,订单信息无处安放。现象是图上只有用户、航班、机票三个类,用户和航班之间画一条多对多,然后问「乘机人姓名存哪」。原因是把「订票关系」当成了两个实体的直接关联。解决办法是显式建一个Order关联类,挂上订单号、乘机人、金额、状态,这条多对多就成了一对多 + 多对一。这个改动看着小,但它直接决定了建表时是三张表还是两张表。

时序图只画成功路径,异常分支全部缺席。现象是登录时序图里只有一条向上的成功返回箭头,实现时不知道该在哪儿抛异常。原因是画图时默认「一切顺利」。解决办法是每条时序图至少补两条异常返回:一条参数或前置校验失败,一条资源状态冲突(比如座位已满)。异常返回不用画全,画到「哪一层负责拦」就够了。

5.2 落地阶段的三个坑

权限只做前端,接口裸奔。现象是页面上看不到管理入口,但直接拿普通用户的会话请求/api/admin/flights居然能成功。原因是路由守卫写在了前端,后端认为「用户既然能发这个请求就一定有权」。解决办法是后端每个接口都过一遍角色校验,管理接口再叠一层op_token二次凭证校验,前端守卫只当体验优化。

余票扣减写成先查后改,压测一跑就超卖。现象是并发下单时seats_left变成负数,或者同一座位卖出两张。原因是SELECT seats_left和UPDATE seats_left中间隔着几十毫秒,两个请求都读到了旧值。解决办法是UPDATE ... SET seats_left = seats_left - 1 WHERE seats_left > 0这种原子写法,或者事务里加SELECT ... FOR UPDATE,两者取其一,别都不做。

退票时间判断用本地时间,跨时区航班全乱。现象是明明还没起飞的航班,退票时提示「已发出」。原因是航班表里存的是当地时间字符串,而now()取的是服务器时区。解决办法是航班起降时间统一存 UTC,或者存带时区的时间戳,比较前先转成同一时区;前端展示时再转回当地时间。这类问题排查起来特别像玄学,日志里两个时间都对,就是比不出来。

6. 二次开发:在现有模型上加一层可运行骨架的做法

这份建模设计最实用的二次开发方向,不是重画图,而是照着它生成一套能跑的最小骨架。我的习惯是分三步走,每一步都对应文档里已经画好的东西。

第一步,把用例规约里的前置校验抽出来做成一个校验层。每条规约的「异常事件流」就是一组断言,把它们写成独立的函数,接口里按顺序调用。这样做的收益是校验逻辑可单测,不用起服务就能跑。第二步,用类图直接生成建表语句和实体类,关系里的多重性决定外键放哪张表——一对多把外键放在多的一方,多对多拆关联表,这个规则照着图走不会错。第三步,用时序图生成服务方法签名,图上的每条消息对应一个方法调用,消息的发送方是调用者,接收方是被调用者,箭头方向就是依赖方向。

验证方式我一般用两层:一层是针对状态机的穷举测试,把订单的五个状态两两组合,验证只允许TRANSITIONS里列出的迁移;另一层是针对权限的越权测试,拿注册用户的会话去请求每一个管理接口,全部应该是 403,漏一个就说明后端少了一层校验。这两层测试写完,这套模型到代码的映射基本就稳了。

有个小技巧值得单独拎出来:把用例 ID 写进接口注释和错误码里。比如用例 6 退票对应错误码40005/40006,排查线上问题时看到错误码就知道回文档哪一页,比翻代码快得多。这套约定一开始只有我自己用,后来团队里其他人接手,第一句话都是「错误码前两位是啥意思」——这说明它确实省事。

从那以后我每次拿到只有建模文档、没有代码的项目,都强制自己先跑一遍「规约→断言、类图→建表、时序图→方法签名」这三步,跑不通就说明模型有缺口,早发现比写完再改便宜太多。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 2:09:10

重读BlueField-2 DPU方案:数据面卸载与裸金属弹性存储

简介&#xff1a;《BlueField DPU Solutions 202104.pdf》围绕 NVIDIA BlueField-2 DPU 的产品方案展开&#xff0c;面向数据中心架构师、云计算与网络虚拟化从业者&#xff0c;以及关注裸金属云和基础设施卸载的技术人员。正文从 DPU 的基本概念入手&#xff0c;依次覆盖安全加…

作者头像 李华
网站建设 2026/10/28 2:09:09

openTCS 5.9中文手册:从零搭建AGV调度系统与首张运输订单

简介&#xff1a;openTCS 5.9中文版用户手册面向AGV调度系统与RCS系统的开发者、集成工程师及运维人员&#xff0c;将开源运输管理平台的核心概念与操作方法译成中文&#xff0c;可降低英文文档的阅读门槛&#xff0c;帮助用户理清模块关系。手册从软件目的、系统要求与许可说明…

作者头像 李华
网站建设 2026/10/28 2:08:55

15页5G+VR智慧旅游方案PPT:VR观景台到数据中台的拆解复用

简介&#xff1a;面向旅游行业信息化从业者、景区运营方与智慧城市方案设计人员的5GVR智慧旅游解决方案演示文稿&#xff0c;聚焦5G网络与虚拟现实技术在旅游场景中的落地路径。内容围绕5G高速率、低时延、大连接特性与VR沉浸式交互体验展开&#xff0c;涵盖智慧旅游系统架构、…

作者头像 李华
网站建设 2026/10/28 2:08:55

S7-1200 PUT/GET通信实战:连对、写对、防错三要素

简介&#xff1a;本资源是一份面向工业自动化工程师与PLC初学者的西门子S7-1200 PUT&GET通信实战教程&#xff0c;聚焦PROFINET环境下两台S7-1200 PLC间高效、稳定的数据交互问题&#xff0c;特别适用于需实现跨设备数据同步、分布式控制或项目升级迁移的工程场景。教程完整…

作者头像 李华
网站建设 2026/10/28 2:08:18

Qlib 量化投资平台实战:从环境搭建到因子选股回测全流程

简介&#xff1a;这份PDF资料是华泰证券金工研究团队2020年12月发布的深度研究报告&#xff0c;聚焦微软AI量化投资平台Qlib的实操体验&#xff0c;面向量化投资研究者、金融从业者及希望降低AI算法使用门槛的学习者。报告以港股日频量价因子LightGBM选股策略为案例&#xff0c…

作者头像 李华
网站建设 2026/10/28 2:07:57

Zemax F/4单透镜与tfcalc六层膜上机复现指南

简介&#xff1a;这份应用光学上机实验报告面向高校光学课程学生、实验课教师及需要撰写同类实验报告的学习者&#xff0c;聚焦Zemax透镜设计与tfcalc光学薄膜分析两项上机任务&#xff0c;可用于实验预习、过程对照和结果复盘。文档围绕F/4、焦距100mm、BK7玻璃透镜&#xff0…

作者头像 李华