UML网上订餐系统设计:需求模型、分析模型与用例实现对齐
简介:《UML网上订餐系统分析与设计》是一份面向软件工程课程实验与UML建模初学者的完整实验报告,围绕网上订餐场景梳理游客注册、用户登录/注销、餐品检索、订单维护及管理员后台等业务流程。文档从需求模型入手,依次给出架构模型、分析机制、关键抽象和用例实现,并配有类图与顺序图,注册、登录等用例还细化了事件流、前置条件、后置条件与备选流,便于对照理解从需求到设计的建模思路。资源包共1个doc文件,约149KB,篇幅集中、结构清晰,可直接作为课程大作业、实验报告撰写或UML建模练习的参考范本。已有4230人学习,适合需要快速掌握用例图、序列图/顺序图和类图表达方式的学生与开发人员查漏补缺。
1. 需求先立住:一份 UML 网上订餐系统设计为什么难在三处对齐
带过几轮大作业之后我发现,UML 网上订餐系统这类题目,真正卡住人的从来不是画不出图,而是三处对不上:需求模型里列了十个用例,分析模型里的类却只剩五个;顺序图里界面层直接连数据库;类图标了 0..1 的多重性,建表时却写成外键非空。评审的人只要顺着一条主流程走下来,三处一断,整份设计就站不住。
这套方案要解决的就是对齐问题:把游客、注册用户、管理员三类角色,餐品、订单、收藏、评论、留言、公告六条业务线,用需求模型、分析模型、用例实现三段式串成一条能从图走到表的链路。它适合正在交 UML 建模大作业、软件工程课程设计的学生,也适合要出一份系统分析文档的初级工程师,照着结构改改就能用。
2. 需求模型:三类角色、十个用例与权限边界的切法
需求模型最容易写成功能清单,十个用例排成一列,谁都能做、谁都做不全。合格的切法是把角色先钉死,再让用例挂到角色上,最后用权限矩阵反向校验一遍有没有漏角色、有没有多角色。
2.1 用例图:角色先定,用例后挂
这份设计的角色只有三类,不要贪多。游客是未登录态,能做的只有注册、浏览餐品目录、看部分留言和公告;注册用户是登录态,比游客多出选购、收藏、评论、订单维护、个人资料修改;管理员是后台态,管餐品、管订单、管用户权限、管留言和公告。三个角色不并列,游客和注册用户是同一实体的不同状态,这一点在类图里会体现为 guest 与 user 的关联,而不是继承。
十个用例按业务归组:账户线有注册、登录与注销、用户信息管理;交易线有餐品信息检索、餐品选购、订单信息管理;内容线有餐品评论、收藏夹管理、留言板管理、公告栏管理;再加一条管理员专属的餐品管理和用户权限管理。归组的意义在于,同一组的用例共享同一批实体类,画顺序图时可以复用控制层对象,不至于十张图画出十种风格。
判断用例粒度有个简单办法:一个用例必须能独立写出一段完整的基本流和至少一条备选流。如果写不出来,说明它只是主用例里的一个步骤,应该降级为子流程。比如「清空订餐车」单独拿出来做用例就过细,它属于餐品选购基本流里的第 3 步。
2.2 用例规约的六要素:以注册和登录为例
用例规约是很多人偷懒的地方,图上画个椭圆就算完。实际评审看的就是规约写得够不够细。六个要素固定为:简要说明、事件流(基本流加备选流)、前置条件、后置条件、特殊要求、扩展点。扩展点可以写「无」,但前五个不能缺。
注册用例的规约重点在备选流。基本流是游客选注册、系统返回注册页、填写用户名密码邮箱电话安全问题、系统校验、提交、提示成功并默认登录。三条备选流必须写全:字段长度超限或格式不符时给提示并退回注册页;用户名已被占用时提示并退回;系统异常时给出维护提示。前置条件是游客发起注册,后置条件是游客成为注册用户。
登录用例要拆成两个角色看。普通用户在前台登录,输入用户名、密码、验证码,校验通过后记录登录态并回主页,可以主动注销。管理员只能从后台登录,不提供管理员注册入口,账号只能在数据库里由运维添加,这一条是安全性设计里最容易被忽略又最容易被问到的点。备选流至少写三条:忘记密码走找回流程、账号密码错误提示重试、管理员越权从前台进入时拒绝。
2.3 include 与 extend 的取舍
这两个关系被滥用的概率极高。判断标准只有一条:被抽出来的行为是不是每次都要执行。每次必执行、且被多个用例共用,用 include,比如「登录校验」被选购、收藏、评论、订单四条线 include,箭头指向被包含用例。有条件执行、执行与否取决于运行时分支,用 extend,比如「找回密码」只在用户点忘记密码时才发生,箭头从扩展用例指向基础用例。
有个反直觉的经验:宁可少画 include。一张用例图上出现五六条 include 线,通常说明作者把「系统内部调用」误当成用例关系了。用例之间是参与者可见的行为关系,不是代码里的函数调用。
2.4 角色权限矩阵:反向校验用
把十个用例和三类角色做成矩阵,横轴角色、纵轴用例,能做的打勾。这张表的价值不在展示,在于校验:每一行至少有一个勾,否则是用例没人用;每一列至少有一个勾,否则角色是多余的;管理员这一列不能出现「注册」,用户这一列不能出现「餐品上架」。
| 用例 | 游客 | 注册用户 | 管理员 |
|---|---|---|---|
| 注册 | 可 | 不可 | 不可 |
| 登录/注销 | 不可 | 可 | 仅后台 |
| 餐品检索 | 可 | 可 | 可 |
| 餐品选购 | 不可 | 可 | 不可 |
| 收藏夹管理 | 不可 | 可 | 不可 |
| 餐品评论 | 仅浏览 | 可评论回复 | 仅浏览 |
| 订单信息管理 | 不可 | 可下单撤销 | 可发货完成 |
| 留言板管理 | 仅浏览部分 | 可留言 | 可回复删除 |
| 公告栏管理 | 可浏览 | 可浏览 | 可增删改 |
| 餐品管理 | 不可 | 不可 | 可 |
| 用户权限管理 | 不可 | 不可 | 可冻结解冻 |
矩阵里「订单信息管理」这一行最有意思,同一用例被两个角色共用但操作不同,这时候不要拆成两个用例,而是在规约的基本流里分段写「用户订单管理」和「管理员订单管理」,顺序图也画两张。这是角色与用例多对多时最干净的写法。
3. 分析模型:四节点架构、六种分析机制与九个关键类
需求模型回答做什么,分析模型回答怎么分。这一层如果只画一张漂亮的类图就交差,后面的顺序图必然接不上,因为没人知道哪个类该承担持久化、哪个类该做安全校验。
3.1 架构模型:四个节点各自管什么
架构图画成 ClientSystem、Server、DBsever、Printer 四个节点。ClientSystem 是浏览器端,只负责界面渲染和输入采集,不写业务判断;Server 承载控制层和信息保护层,所有校验、状态流转、权限判定都在这里;DBsever 承担持久化,只有信息保护层能连它;Printer 是订单出票的外部设备,通过 Server 的打印接口对接。
四个节点之间的连线是有方向的,这一点很多人画错。客户端到服务器是请求方向,服务器到数据库是读写方向,服务器到打印机是单向输出。客户端不能直连数据库,这条在顺序图上体现为界面层永远只发消息给控制层,绝不出现「界面层 → 数据库」的消息箭头。
Printer 单独成节点常被质疑多余,其实它是「遗留接口」分析机制的载体。现实中的订餐系统出票机走的是并口或串口协议,属于典型的 legacy interface,把它画成独立节点,正好说明系统为什么需要一个适配层去包装它,而不是让控制层直接调硬件。
3.2 分析机制:把横切关注点挂到类上
原文列了六种机制:Persistency、Security、Distribution、Communication、Parsing、Legacy interface、Redundancy。逐个对上号:orderlist、order、dish、user、guest、favorite、notice-board、comment 都带 Persistency,因为这些实体都要落库;order、system、user、guest 带 Security,因为涉及权限和身份;dish 带 Distribution,因为餐品信息要分发到前台展示和管理员后台;orderlist、notice-board、comment 带 Communication,因为它们承载用户与系统之间的消息;comment 带 Parsing,评论内容要做长度和敏感词解析;系统整体带 Legacy interface,对应打印机这类老设备。
这一步在文档里通常画成一张机制对照表,两列:分析类、涉及机制。它的作用是给后续的类设计加约束:凡是标了 Persistency 的类,类图上必须能对应到一张实体表;凡是标了 Security 的类,顺序图里必须出现信息保护层的校验消息。
| 分析类 | 涉及机制 |
|---|---|
| orderlist | Persistency、Security |
| order | Persistency、Security |
| dish | Persistency、Distribution |
| user | Persistency、Security |
| guest | Persistency、Security |
| favorite | Persistency、Communication |
| notice-board | Persistency、Communication |
| comment | Persistency、Parsing |
| system | Persistency、Legacy interface |
3.3 九个关键类与多重性的取值依据
关键抽象定为九个:guest、user、system、dish、orderlist、order、favorite、comment、notice-board。多重性标注是这份模型里含金量最高的部分,因为每一个 0..* 和 0..1 背后都对应一条业务规则。
guest 与 comment 是 0..* 对 0..1:一个游客可以发零到多条评论(实际系统里通常要求登录后才能评论,这里保留游客浏览语义),一条评论只属于一个发布者。user 与 order 是 0..* 对 0..1:一个用户可以有多张订单,一张订单只属于一个用户,0..1 表示订单尚未生成时用户可以没有订单。order 与 dish 通过 orderlist 建立 0..* 对 0..1 的间接关联,一张订单包含多条明细,一条明细对应一个餐品。user 与 favorite 是 0..* 对 0..1,收藏条目必须挂在用户名下。dish 与 comment 是 0..* 对 0..1,一个餐品可以有多条评论。
容易标错的是 order 与 dish 之间。如果直接画成多对多,就会丢掉「买了几个」这个信息,正确做法是引入 orderlist 作为关联类,把数量、单价、折扣放进去。这也是后面建表时订单主表加订单明细表两张表的模型依据。
| 关系 | 多重性 | 业务含义 |
|---|---|---|
| user—order | 0..* 对 0..1 | 一人多单,单归一人 |
| order—orderlist | 0..* 对 0..1 | 一单多明细 |
| orderlist—dish | 0..* 对 0..1 | 明细指向具体餐品 |
| user—favorite | 0..* 对 0..1 | 一人多收藏 |
| dish—comment | 0..* 对 0..1 | 一菜多评论 |
| guest—comment | 0..* 对 0..1 | 一条评论一个发布者 |
3.4 类图落地:属性、方法与可见性对照
原模型的类属性里存在明显的类型误用,比如 telephone 写成 int、time 写成 int。落在文档里要修正:telephone 用字符串,因为要保留前导零和区号;时间用 datetime;价格用 decimal 而不是 double,避免浮点误差。dish 类的 markprice 是市场价、dishprice 是网订价,两个价格字段并存是为了展示折扣,discount 不单独存,由两个价格相除得出。
方法部分按职责给:user 类提供 register、login、logout、updateProfile、changePassword;dish 类提供 search、list、update;order 类提供 create、cancel、confirm;orderlist 类提供 addItem、removeItem、clear、calcTotal。可见性上,属性一律 private,方法对外 public,只在 calcTotal 这类内部计算上用 private。这套约定写进文档,后面写代码就能直接照着实现。
4. 用例实现:从顺序图的消息编号到控制层与信息保护层的落地
顺序图是把模型翻译成实现的最后一公里。十条主流程每条都要有图,但图的骨架是一致的:左边参与者,中间控制层,右边信息保护层,最右数据库或外部设备。消息必须编号,编号顺序就是调用顺序,也是后面排查问题的依据。
4.1 三层边界:界面层、控制层、信息保护层
界面层只做两件事:采集输入、展示结果。所有判断都不放在这里。控制层是业务大脑,负责参数校验、状态判定、调用持久化。信息保护层是唯一的数据库入口,做 SQL 组织、事务控制、加解密。
一个典型错误是把「验证用户名是否已存在」画在界面层。正确的位置是控制层收到注册请求后,先调信息保护层的 queryByUsername,拿到结果再决定返回成功还是提示重名。顺序图上这条消息的编号一般是 2 到 5 之间,顺序是:提交信息、校验格式、查重、写库、返回。
4.2 注册与登录:消息编号与校验顺序
注册的顺序图固定七步:用户选择注册界面、填写注册信息、提交、控制层做字段校验、信息保护层查重并写入、返回保存结果、回显注册成功。校验顺序不能乱,先做本地格式校验再做数据库查重,因为格式不符的请求不应该打到数据库。登录同理,先校验验证码,再校验账号密码,最后写登录日志,把最便宜的校验放最前面。
密码存储这一层在模型里体现为 Security 机制挂到 user 类上,实现时对应的是信息保护层里的加密方法。文档里不需要写具体算法,但要写明「密码不以明文落库,注册时加密、登录时比对摘要」,这条写进去,答辩时就是加分项。
# 注册请求处理:控制层,校验顺序不能颠倒 def handle_register(form): # 1. 本地格式校验,不碰数据库,成本最低 for field in ("username", "password", "email", "telephone", "security_question"): if not form.get(field): return {"code": 400, "msg": f"{field} 不能为空"} # 2. 长度与格式规则,避免超长字段写入 if not (6 <= len(form["username"]) <= 20): return {"code": 400, "msg": "用户名长度需在 6 到 20 之间"} if not re.match(r"^1[3-9]\d{9}$", form["telephone"]): return {"code": 400, "msg": "手机号格式不正确"} # 3. 查重放在最后,只有格式合法的请求才访问数据库 if user_dao.find_by_username(form["username"]): return {"code": 409, "msg": "该用户名已被注册"} # 4. 密码摘要后落库,明文不进入持久层 user_dao.insert( username=form["username"], password_hash=hash_password(form["password"]), email=form["email"], telephone=form["telephone"], security_question=form["security_question"], ) return {"code": 200, "msg": "注册成功"}这段代码对应顺序图里控制层的前四条消息,hash_password 和 user_dao 分别落在信息保护层的两个职责里。参数上要注意 username 唯一索引、telephone 用 varchar(20),这两个在建表时就要定死,否则后面改表代价很大。
4.3 餐品选购与订单结算:合并规则与总价计算
购物车是这份设计里逻辑最密的地方。基本流五步:加入餐品、移除餐品、清空、价格统计、结算生成订单。关键的合并规则是:同一餐品重复加入不新增条目,只把数量累加。这一条不写清楚,实现时就会出现同一菜品两行明细的翻车现场。
结算时把购物车快照写进订单明细,而不是只存 dish_id。原因很实在:餐品价格会变,订单要保留成交时的价格。这条规则在类图上体现为 orderlist 冗余存储单价字段,在表结构上体现为订单明细表同时存 dish_id、dish_name、unit_price 三个快照字段。
-- 订单主表:一次结算一条 CREATE TABLE orders ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0, -- 0待发 1已发 2已完成 3已撤销 receiver VARCHAR(32) NOT NULL, telephone VARCHAR(20) NOT NULL, address VARCHAR(200) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_status (user_id, status, created_at) ); -- 订单明细表:价格与名称做快照,餐品下架不影响历史订单 CREATE TABLE order_item ( item_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, dish_name VARCHAR(64) NOT NULL, unit_price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL DEFAULT 1, KEY idx_order (order_id) );idx_user_status 这个联合索引对应的是管理员端「按状态分类检索、按生成时间降序」这个需求,索引里 created_at 放最后,正好支撑 order by 的排序不走文件排序。这是模型层看不出来、实现层必须补的一笔。
4.4 管理员订单管理:五态流转与不可逆约束
订单状态在设计里定四态加一个初始态:待发、已发、已完成、已撤销。流转规则是单向的:待发可以转已发或已撤销,已发可以转已完成或已撤销,已完成和已撤销是终态,不能再变。撤销后的订单不从记录里删除,只改状态,这样历史查询还能查到。
# 订单状态机:把允许的流转写死,非法流转直接拒绝 ALLOWED_TRANSITION = { 0: {1, 3}, # 待发 -> 已发 / 已撤销 1: {2, 3}, # 已发 -> 已完成 / 已撤销 2: set(), # 已完成是终态 3: set(), # 已撤销是终态 } def transit(order_id, target_status): current = order_dao.get_status(order_id) if current is None: return {"code": 404, "msg": "订单不存在"} if target_status not in ALLOWED_TRANSITION[current]: # 典型场景:已完成的订单想撤销,必须拒绝 return {"code": 409, "msg": f"状态不允许从 {current} 流转到 {target_status}"} order_dao.update_status(order_id, target_status) return {"code": 200, "msg": "状态更新成功"}用户侧和管理员侧的撤销权限要分开:用户只能在管理员发货前撤销,也就是待发态;管理员可以撤销待发和已发两态,但已发撤销要留操作日志。这条规则不写进状态机,就会变成线上事故。
5. 避坑与排查:六个让模型和实现对不上的高发问题
5.1 建模阶段的三个坑
现象一:用例图画了十二个椭圆,类图只剩七个类,顺序图想画却找不到对象。原因是用例是按页面切的,不是按业务行为切的。解决办法是把每个用例回溯到它的主实体,一个用例至少对应一个关键类,对应不上的说明它只是一个页面跳转,合并到父用例里。
现象二:多重性全标成 0..* 对 0..*,看起来通用,一建表全是中间表。原因是作者没想清楚业务上的一对多关系。解决办法是逐条问三个问题:一条 A 能不能属于多条 B、一条 B 能不能属于多条 A、这条关联上有没有需要携带的属性。第三个问题答「有」,就说明要引入关联类,比如 orderlist。
现象三:顺序图里界面层直接连数据库,或者控制层自己拼 SQL。原因是图是照着功能写的,不是照着架构写的。解决办法是先定死三层边界,界面层只发消息给控制层,控制层只发消息给信息保护层,画完之后拿这个规则扫一遍每张图,越界的连线全部重画。
5.2 数据与状态阶段的三个坑
现象四:删除餐品时把历史订单里的菜名一起删没了。原因是订单明细只存了 dish_id,没有做快照。解决是在明细表里冗余 dish_name 和 unit_price,删除餐品只做逻辑删除,改状态字段,物理行不动。
现象五:注册接口被刷,数据库里出现几十条同用户名记录。原因是查重和插入之间没有唯一约束兜底。解决办法是给 username 加唯一索引,代码里查重是为了给友好提示,数据库约束才是最后一道防线,两者不能互相替代。
现象六:用户已完成的订单被撤销,金额对不上账。原因是状态流转没有写死。解决是把状态机抽成常量表,任何状态变更都走同一个 transit 函数,非法流转直接返回 409,不允许在其他地方直接 update status。
排查顺序建议固定为四步:先看顺序图里消息编号是否连续、有没有跳跃;再核对类图多重性和表结构的外键是否一致;然后检查状态字段的取值集合和代码里的常量是否对得上;最后扫一遍权限矩阵,确认每条接口的角色校验都落到了信息保护层而不是前端。
提示:评审最常问的三个问题是「这个 0..1 为什么不是 1..1」「删除餐品后历史订单还能不能查」「管理员能不能从前台登录」。三个都答得上来,模型基本就站住了。
6. 验收与答辩:用一张对照表证明模型能驱动实现
验收这件事,别等最后一页 PPT 才想起来。我的做法是在文档定稿前,自己先做一张「模型—实现」对照表,横轴是模型产物,纵轴是实现产物,逐行填,填不满的行就是要在答辩前补的洞。
| 模型产物 | 对应实现产物 | 验收要点 |
|---|---|---|
| 用例图十个用例 | 十个接口或页面入口 | 每个用例能找到至少一个入口 |
| 角色权限矩阵 | 接口级角色校验 | 越权请求返回 403 |
| 类图九个类 | 九张表或实体类 | 属性名、类型能一一对上 |
| 多重性标注 | 外键与索引 | 0..* 侧建索引,0..1 侧建外键 |
| 顺序图消息编号 | 控制层方法调用链 | 编号顺序与调用栈一致 |
| 状态流转规则 | 状态机常量表 | 非法流转被拒绝并有日志 |
| 分析机制对照表 | 加密、事务、打印适配层 | 每项机制有落点代码 |
填表时有个小技巧:从右往左填。先列出你实际写了哪些接口、建了哪些表,再回到模型里找对应项,找不到的就是模型缺失,找到两个以上对应的就是模型冗余。这个方法比从左往右填更容易发现「图很好看但没落地」的问题。
答辩陈述按四段走,每段控制在一分半。第一段讲清楚三类角色和权限边界,用权限矩阵那页;第二段讲架构四节点和三层边界,重点强调客户端不直连数据库;第三段挑一条主流程,从用例规约讲到顺序图再讲到建表,注册或下单都行,讲完这条流程,可信度就有了;第四段主动暴露一个设计取舍,比如「订单明细做价格快照是因为餐品会调价」,主动讲比被问出来强得多。
被追问时别急着解释图,先回答业务规则,再回到图上指位置。评审问「为什么这个关联是 0..1」,实质是在问业务上允许不允许为空,答「订单未生成时用户没有订单,所以是 0..1」比答「UML 规范这么定义」有用得多。
注意:文档里所有类型错误都要提前修掉。telephone 写成 int、时间写成 int 这类问题,一眼就会被看出来,修起来只要十分钟,不修就是整份文档的专业性被打折。
我第一次交这份作业时,类图上标了漂亮的多重性,建表却全写成外键非空,答辩时被问「游客没有订单怎么插数据」,当场卡住。从那以后我每次做完模型都强制走一遍「模型—实现」对照表,填不满的行绝不往下推。希望帮到你。
本文还有配套的精品资源,点击获取