简介:这是一份面向软件工程课程实验与UML建模练习的网上订餐系统分析与设计实验报告,适合需要完成需求建模、用例文档和类图训练的本科生或初学者参考。压缩包内共1个doc文件,大小约149KB,内容围绕网上订餐业务展开,涵盖需求模型、架构模型、分析机制、关键抽象、用例实现及顺序图等部分。报告以游客注册、登录注销、餐品检索等典型场景为例,整理了事件流、前置条件、后置条件、类设计与类间关联,并配有注册、登录等顺序图,便于读者对照理解UML从需求到设计的表达方式。已有4229人学习,可作为课程大作业的结构模板和建模思路参考,帮助快速梳理角色权限、订单与餐品等核心对象关系,减少从零搭建文档框架的时间。
1. 从一份“跑不起来”的建模文档说起
朋友把一份 UML 网上订餐系统的建模文档甩过来时,原话是“这东西一行代码没有,你能看出什么”。结果翻到第三页就发现不对:注册用例的备选流写了用户名重复、字段超长、系统维护三种分支,连顺序图的四列对象都排好了——界面、控制层、信息保护层、数据库。这哪是交作业,这分明是一份可以直接照着写接口的需求基线。这份资源的价值不在“能跑”,而在“能照着建”:它把游客、注册用户、管理员三类角色的权限边界,十二个用例的事件流,九个核心类之间的多重性关系,以及一套 Client–System–DBserver–Printer 的架构模型全部落到了纸面。适合两类人:正在补 UML 建模作业、需要一份完整结构当骨架的;以及准备做订餐类小系统、想先要一份能当需求说明书用的建模底稿的。
2. 需求模型拆解:三类角色与十二个用例是怎么切出来的
需求模型这一层决定后面所有图对不对。这份文档的思路很清晰:先切角色,再挂用例,最后给每个用例套一个固定模板。顺序反过来做,图就会画成一锅粥。
2.1 角色划分:游客、注册用户、管理员的权限边界
文档里出现的角色有四个:游客、注册用户、管理员、经理。前三者的边界划得比较死,经理是个漏网的。
游客的能力被压得很低:能注册、能浏览部分留言、能看餐品评论和公告栏,但没有收藏夹、不能下单。文档里收藏夹用例的备选流写得很直接——“未登录的游客不会拥有收藏夹功能”,这种把否定条件写进备选流的做法比在类图上标个注释管用得多,因为开发看到这句话就知道要在收藏接口上加登录拦截。
注册用户是主干角色,订餐全链路都压在他身上:餐品检索、加购、收藏、评论、留言、个人信息维护、订单撤销。管理员则是另一条完全隔离的线,只能后台登录、不开放注册,文档原话是“管理员只能在数据库中添加,以保证系统的安全性”。这句话在落地时对应的是一个很具体的实现:管理员表不挂注册接口,初始账号靠初始化脚本插入。
一个人做订餐系统最容易翻车的地方,是把管理员和用户的登录做成同一个入口、同一张表加个 role 字段。这份文档明确把两条线分开,好处是权限判断可以从“查字段”简化成“查来源”,后台接口天然只信任后台会话。缺点是后面订单状态流转要跨两条线,管理员发货、用户确认收货,得有一套共享的订单状态定义,这一点文档里只给了状态名没给状态机,第四章会补上。
经理这个角色只在需求模型里出现过一次,之后所有用例、所有顺序图里再没露面。这不一定是错,可能是文档合并时留下的残影,但它会直接导致评审时被追问“经理到底能干什么”。处理办法有两个:要么在用例图里补一个“查看经营报表”之类的用例把它挂上,要么干脆删掉。留着不处理是最差的选择,后面做权限矩阵时会出现一个没有任何接口对应的角色。
2.2 用例清单与用例图复现
十二个用例摊开是这样的:注册、登录/注销、餐品信息检索、留言板管理、收藏夹管理、餐品选购、餐品评论、用户权限管理、用户信息管理、公告栏管理、管理员餐品管理、订单信息管理。这里面有三个是管理员专属——用户权限管理、管理员餐品管理、订单信息管理(后台部分),剩下九个是用户侧或双端共用。
用 PlantUML 把这张用例图重画一遍,比在 Word 里拖框稳得多,改一行文本就重出一张图,还能进版本库对比差异:
@startuml left to right direction actor 游客 as Guest actor 注册用户 as User actor 管理员 as Admin rectangle 网上订餐系统 { usecase "注册" as UC1 usecase "登录/注销" as UC2 usecase "餐品检索" as UC3 usecase "餐品选购(购物车)" as UC4 usecase "收藏夹管理" as UC5 usecase "餐品评论" as UC6 usecase "留言板管理" as UC7 usecase "公告栏查看" as UC8 usecase "用户信息管理" as UC9 usecase "订单信息管理" as UC10 usecase "管理员餐品管理" as UC11 usecase "用户权限管理" as UC12 } Guest --> UC1 Guest --> UC3 Guest --> UC6 Guest --> UC8 User --> UC2 User --> UC3 User --> UC4 User --> UC5 User --> UC6 User --> UC7 User --> UC9 User --> UC10 Admin --> UC2 Admin --> UC7 Admin --> UC8 Admin --> UC10 Admin --> UC11 Admin --> UC12 @enduml这段代码里left to right direction是排版指令,让三个角色竖排在左、用例横排在右,图不会拉得太宽;rectangle划出系统边界,边界外的 actor 才是外部角色。每个-->就是一条参与者与用例的关联,没有箭头方向的含义,只是连线。真正需要核实的是连线本身:比如Guest --> UC3表示游客也能检索餐品,这和文档里“用户能通过输入检索条件检索餐品”并不冲突,因为检索本身不需要登录,只有加购才需要。
改这张图的时候有个习惯值得养成:每加一条连线,回头去对应用例的“前置条件”里找依据。找不到依据的连线,要么是漏写了前置条件,要么是这条连线本来就不该存在。
2.3 六段式用例模板:基本流、备选流、前置后置条件
十二个用例全部套了同一个模板:简要说明、事件流(基本流 + 备选流)、特殊要求、前置条件、后置条件、扩展点,最后附顺序图。这个模板本身不稀奇,稀奇的是备选流写得够细。
拿餐品选购来说,基本流五步走完加购、移除、清空、算价、结算,备选流直接列了七条:未登录返回登录页、系统异常、非会员要求先登录、订餐为空提示、修改数量未变提示无更改、用户取消修改、购物车为空。这七条里至少有四条是实际开发中会被漏掉、上线后被用户点出来的分支。比如“修改数量没有改变,提示无更改并返回购物车页面”,这种细节能写进需求文档,说明写的人真的点过这个按钮。
再看注册用例的前置条件只有一句“游客申请注册”,后置条件是“游客注册成功成为会员”。前置后置写得短是好事,但短到没有可验证性就有问题——“注册成功”到底以什么为准?是数据库里插入了记录,还是返回了登录态?文档在基本流第六步补了一句“系统提示注册成功并返回首页(默认已登录)”,这句括号才是真正的后置条件。所以读这类模板时,别只看“后置条件”那一栏,基本流的最后一步往往藏着真正的验收标准。
特殊要求这一栏大部分用例写的是“无”,但餐品信息检索写了“要求在网站上提供所属菜系、主要材料以及实例图片以方便用户选择查看”。这一条直接决定了 dish 类要带material字段,也让检索接口的返回体里必须包含图片地址。很多人建模时把这一栏当摆设,实际上它是少数几个能约束前端展示的地方,值得认真填。
3. 分析模型:四层架构与九个关键抽象怎么落地
需求模型回答“做什么”,分析模型回答“拆成几块、每块挂什么职责”。这份文档的分析模型分三部分:架构模型、分析机制、关键抽象。顺序不能乱,先有架构再谈机制,最后才落到类。
3.1 架构模型:Client、System、DBserver、Printer 四层的职责
架构模型给出的组件只有四个:Client、System、DBserver、Printer。画成一条链就是客户端连系统、系统连数据库、系统连打印机。
这四个组件里最值得说的是 Printer。一个网上订餐系统为什么要在架构层画打印机?看订单用例就明白了:用户提交订单后“系统会再次确认送达信息,用户点击确认之后递交数据库,提示操作完成,并打印出这张订单的所有信息”。这里的打印机是商家侧的出单设备,订单落库之后要即时出小票。把它画进架构模型,等于提前声明了“订单写入”和“订单打印”是两个动作,不能合并成一个事务——打印失败不该回滚订单,订单回滚也不该因为打印机没纸而阻塞。这个判断在后面对接真实打印服务时非常关键,血泪经验是:把打印塞进数据库事务里的系统,高峰期一定会因为出单慢把连接池占满。
System 这一层是唯一的业务中枢,Client 只管展示和收集输入,DBserver 只管存。这种划分在文档里体现为顺序图的四列对象:用户 → 界面 → 控制层 → 信息保护层。控制层归 System,信息保护层(也就是持久层)跨在 System 和 DBserver 之间。四列的分法朴素但管用,它强制每个用例都必须回答一个问题:这一步的逻辑归谁。加购物车归控制层,扣库存归信息保护层,界面只负责把数量传上去。
如果要把这套架构映射到现代 Web 项目,我一般这样对:Client 对应浏览器或小程序端,System 对应后端服务(Controller + Service),DBserver 对应 MySQL 或 PostgreSQL,Printer 对应一个独立的打印微服务或消息队列消费端。四层变四块,职责边界一点没变。
3.2 关键抽象:九个类与它们的多重性关系
关键抽象这一节给出了九个类:guest、user、administrator、system、dish、order、orderlist、favorite、comment、notice-board。数一数其实是十个,文档标题写“关键抽象”时把 system 也算进去了。
这十个类之间的关系用多重性标得很密,几乎每个类对其他类都是0..*对0..1。这种“多对零一”的标注在订餐场景里确实常见:一个用户可以有零个或多个订单,一个订单只属于一个用户;一个订单可以包含多个餐品明细,一条明细只对应一个订单。但全部标成0..*到0..1就有偷懒的嫌疑——收藏夹和餐品之间应该是多对多,一个餐品能被多个用户收藏,一个用户能收藏多个餐品,中间需要一个关联实体来承载“收藏时间”这类属性。
dish 类的字段值得单独看,因为它是全系统字段最多的类:
@startuml class dish { dishID : int dishname : string dishprice : double markprice : double meat : boolean cooking : string material : string discription : string hot : boolean recommend : boolean } class orderlist { username : string ordernum : int time : int foodname : string foodnum : int fooddiscout : double items : arraylist } dish "0..*" -- "0..1" orderlist @endumldishprice和markprice是网订价与市场价,两个价格并存的目的是让前端能画出划线价,这个设计在电商里很常见,但要注意两者都是金额,用浮点存是隐患,后面第五章会细说。meat是布尔值表示荤素,cooking存做法(蒸煮炒炸),material存主要材料,hot是销量热度,recommend是店长推荐。这七个字段基本覆盖了检索用例要求的“所属菜系、主要材料、实例图片”里的两项,唯独缺图片字段——这是一个典型的需求与类设计脱节的地方,检索用例的特殊要求要图片,类里没有对应属性。落地时必须补一个image_url。
orderlist 类的字段也很有意思:username、ordernum、time、foodname、foodnum、fooddiscout、items。这个类同时承担了“购物车行”和“订单行”两个角色,items是个arraylist,说明它自己还能再套一层。这种设计在建模阶段能省事,实现阶段会打架:购物车里的临时数据要不要落库?落了库,用户放弃下单后这些行怎么清理?不落库,items这个集合又挂在哪。我的建议是把 orderlist 拆成 cart_item 和 order_item 两个实体,购物车行带会话标识,订单行带订单号,两者字段高度重合但生命周期完全不同。
3.3 分析机制矩阵:Persistency、Security 怎么挂到类上
分析机制这一节是整份文档里最容易被跳过的部分,也是最像“架构师视角”的部分。它给每个类挂了若干机制标签:
| 类 | 挂载机制 |
|---|---|
| orderlist | Persistency、Security |
| system | Persistency、Legacy Interface |
| order | Persistency、Security |
| dish | Persistency、Distribution |
| user | Persistency、Redundancy |
| guest | Persistency、Security |
| favorite | Persistency、Communication |
| notice-board | Persistency、Communication |
| comment | Persistency、Parsing |
每个类都挂了 Persistency,这没什么争议——所有业务实体都要落库。真正有信息量的是其余几项。
order 和 orderlist 挂 Security,因为订单涉及收货地址、联系电话,属于个人信息,读写都要鉴权,用户只能看自己的订单。user 挂 Redundancy,说明用户表是热点,需要冗余或读写分离,这个判断在订餐系统里是对的:登录鉴权每次请求都要查用户,不冗余会打爆主库。comment 挂 Parsing,因为评论内容要过滤敏感词和提取 @ 用户,属于文本解析场景。favorite 和 notice-board 挂 Communication,因为收藏变动和公告发布都需要通知,前者可以推“收藏的餐品降价了”,后者是广播。
dish 挂 Distribution 这一条最有意思。餐品信息是典型的读多写少,而且允许短暂不一致——店长改了价格,晚几秒在所有客户端生效没人会投诉。挂 Distribution 意味着可以做多级缓存、CDN 图片、甚至把餐品目录预生成成静态 JSON。这条机制标签如果被认真执行,检索接口的 QPS 能上一个量级。
system 类挂了 Legacy Interface,说明系统需要对接既有的老接口,比如支付通道或配送系统。这条机制在建模阶段就写出来,等于提醒后面做接口设计时要留适配层,别把老接口的字段直接透传到前端。
看这类分析机制表有个技巧:不要只看挂了什么,要看哪个类挂得最少。这份文档里 administrator 类没有出现在机制表里,但它明明承载了后台登录和权限管理,Security 这一项是必须挂的。漏挂的原因很可能是机制表是照着关键抽象列表抄的,而 administrator 在那张图里被画成了 user 的子类。
3.4 全部类图的复现与核对
把九个类拼成一张完整类图,重点核对三处:属性类型、多重性、继承关系。
@startuml class user { username : string password : string telephone : int useraddress : string usercity : string } class administrator { name : string password : string } class guest { name : int } class order { ordernum : int time : int } class favorite { username : string dishname : string } class comment { title : string content : string name : string telephone : int } class noticeboard { title : string content : string activity : string activitytime : int } class system { username : string password : string } user "1" -- "0..*" order user "1" -- "0..*" favorite user "1" -- "0..*" comment administrator --|> user @endumladministrator --|> user是继承,画出来能省掉一堆重复属性,但代价是管理员登录和用户登录会走到同一个鉴权入口,和文档里“管理员不提供注册、只能数据库添加”的隔离要求正好相反。这里我倾向于不继承,两个类平级,各带各的登录方法,权限判断靠会话来源而不是类型判断。
guest的name : int几乎可以确定是笔误,游客名字是字符串。comment里带telephone : int也多余,评论展示不需要暴露电话。这两处都是抄图时留下的,改起来一分钟的事,但如果带着错进开发,ORM 生成实体时就会多出一个没人用的整型字段。
4. 用例实现与顺序图:界面、控制层、信息保护层怎么串
用例实现这一章是整份文档最厚的部分,十二个用例每个都配了事件流和顺序图。顺序图的结构高度统一,都是四列对象:用户或管理员、界面、控制层、信息保护层。这种统一性是好事,说明作者有一套固定套路,读者照着套就能补出新用例。
4.1 顺序图的统一骨架:七步调用链
把十二张顺序图叠在一起看,会发现它们共享同一个七步骨架:
- 用户选择某个界面
- 用户填写或触发操作
- 界面把请求提交给控制层
- 控制层做校验,转交信息保护层
- 信息保护层落库并返回结果
- 结果逐层回传
- 界面提示用户
注册用例的顺序图就是这个骨架的教科书版:1:选择注册界面 → 2:填写注册信息 → 3:提交注册信息 → 4:保存用户信息 → 5:用户信息保存成功 → 6:用户信息保存成功 → 7:返回给用户注册的信息。第五步和第六步是同一个消息在两层之间的重复回传,编号分开写是为了强调“保存成功”这个状态在每层都要被确认,而不是控制层收到就完事。
这套骨架的隐含约定是:所有业务校验都在控制层,所有数据操作都在信息保护层,界面不做判断。这个约定在订单用例里被打破了——文档写“系统会再次确认送达信息”,这个“确认”发生在哪一层没写清楚。如果放在信息保护层,那它就是一次数据库唯一性检查;如果放在控制层,那它就是参数校验。落地时必须选一个,我的做法是收货信息格式校验放控制层,订单号防重放信息保护层,两层各管各的。
4.2 注册与登录注销:鉴权链路的落地细节
注册用例的六项必填信息是:登录用户名、密码、联系电子信箱、联系电话、安全问题、安全答案。六个字段对应三种校验:用户名唯一性、密码强度、邮箱与电话格式,安全问题用来找回密码。
但把这条要求和 user 类对一遍,会发现 user 类只有username、password、telephone、useraddress、usercity,缺email、securityquestion、securityanswer。而 notice-board 类里反而塞了username、password、email、telephone、securityquestion——注册要的那几个字段全跑到公告栏类里去了。这是一次明显的复制粘贴串行,第五章会专门讲怎么排查这类问题。
登录用例的备选流里有一条很实用:“用户忘记密码,选择找回密码功能,进入找回密码用例”。这说明找回密码本身是一个独立用例,只是没在本章展开。实现上它对应一条带时效的令牌:用户提交用户名 → 系统按注册邮箱发令牌 → 用户凭令牌重置密码。文档给出了入口,没给流程,这块要自己补。
管理员登录有一条硬约束:“管理员只能在后台运行”。这句话翻译成代码就是后台接口统一挂/admin前缀,加一层拦截器校验会话里的角色标记,普通用户会话访问直接 403。别在这一层做“普通用户也能进后台但只能看不能改”的灰度设计,那会把权限判断散到每个接口里。
4.3 购物车到订单:一条带状态机的链路
订单信息管理是十二个用例里唯一带状态流转的。文档把订单分成五种状态:所有订单、待发订单、已发订单、已完成订单、已撤销订单。用户侧和管理员侧各自能触发的动作不同:
- 用户:提交订单、确认收货、撤销未发货订单
- 管理员:检索订单、发货、撤销未发货订单
状态迁移的规则散落在事件流里,没有集中画出来。把它整理成一张迁移表,实现时就不容易漏分支:
| 当前状态 | 触发动作 | 触发方 | 目标状态 |
|---|---|---|---|
| 待发 | 管理员发货 | 管理员 | 已发 |
| 待发 | 撤销订单 | 用户/管理员 | 已撤销 |
| 已发 | 用户确认收货 | 用户 | 已完成 |
| 已完成 | 无 | — | 终态 |
| 已撤销 | 无 | — | 终态 |
这张表有个关键约束:已发订单不能再被撤销。文档原文是“在尚未对该订单进行发送处理之前,管理员可以根据实际情况撤销订单”,这句话就是这条约束的出处。实现时如果只在界面上把撤销按钮置灰,接口层没拦,用户绕过前端直接调接口照样能撤销,库存和账目就乱了。
另一个容易漏的点是“删除餐品后订单记录仍保留”。管理员餐品管理的备选流写了“删除以后,对应的该餐品的订购记录仍然保留,方便日后查询”。这意味着 order 表里不能只存 dishID 外键,必须冗余存一份下单时的菜名和单价快照,否则餐品一下架,历史订单就显示成空白。这是订餐类系统的通用做法,和电商订单快照是同一套逻辑。
4.4 用 PlantUML 把顺序图变成可维护资产
Word 里画的顺序图改一次要重拖半天,用文本描述就没这个负担。把注册用例的顺序图翻译成 PlantUML:
@startuml actor 用户 as U participant "注册界面" as V participant "控制层" as C participant "信息保护层" as P U -> V : 1. 选择注册 U -> V : 2. 填写注册信息 U -> V : 3. 提交注册信息 V -> C : 4. 校验并转发 C -> P : 5. 保存用户信息 P --> C : 6. 保存成功 C --> V : 7. 注册成功 V --> U : 8. 返回首页(已登录态) @endumlparticipant定义生命线顺序,从左到右就是调用方向;->是同步调用,-->是返回;冒号后面的文字就是消息名。改流程时只动文本行,排名和箭头自动重排。
这套文本化还有一个隐性收益:图能进 Git。每次改需求,diff 里能直接看出哪个消息被删了、哪个参与者被换掉了,比对着两张截图找不同靠谱得多。顺序图这东西本来就是给评审看的,能进版本库才算真正被维护起来。
5. 避坑与常见问题排查:这份文档最容易翻车的五处
建模文档的错误不会在编译期报出来,只会在一两个月后以“接口对不上”“数据溢出”“状态乱跳”的形式浮出水面。下面五条都是从这类文档里实际踩出来的。
5.1 游客类的 name 定义成 int
现象:照着类图生成实体,guest 表多出一个name int字段,前端注册游客信息时传字符串直接 500。
原因:抄图时把 username 的类型顺手写成了 int,或者从别的数值型字段复制过来没改。类图不是代码,写错了不会报错,只有生成代码那一刻才暴露。
解决:所有姓名、昵称、用户名一律 string,并且给出长度约束。建模阶段可以在类图旁加一行注释标注长度,比如name : string [1..32],这一行注释比后面改十处代码便宜。
5.2 公告栏类里塞进了注册字段
现象:notice-board 类的属性列表里出现username、password、email、telephone、securityquestion,而真正需要这些字段的 user 类反而没有 email 和安全问题。
原因:类图绘制时从一个类复制到另一个类,粘贴后只改了类名没改属性,这在手绘工具里几乎必然发生。
解决:类图画完做一次“字段归属核对”,方法是拿每个用例的必填项去反查——注册用例要六项,就在全图里搜这六个字段名出现在哪个类下,出现在两个以上的类里就一定是错的。这一步五分钟能做完,能挡掉后面建表时的字段冗余。
5.3 电话用 int、时间用 int、金额用 double
现象:手机号存进telephone int之后变成负数或溢出报错,订单时间显示成一串数字,金额算出来差几分钱。
原因:三个类型选错各有各的理由。手机号 11 位,最大值 19999999999,超过 int 上限 2147483647,溢出是必然的;时间用 int 通常是想存时间戳,但没约定单位是秒还是毫秒,跨语言对接必翻车;金额用 double 是浮点精度问题,0.1 + 0.2 不等于 0.3 这种事在结算页面上会被用户截图。
解决:电话和所有证件类编号一律 string,顺带解决前导零丢失;时间用 datetime 或 timestamp,如果非要用整数就在字段名里写清单位,比如created_at_ms;金额用 decimal(10,2),Java 侧对应 BigDecimal,Python 侧对应 Decimal。这三条改起来成本极低,改晚了要洗数据。
5.4 顺序图编号混乱、层次命名不统一
现象:有的顺序图写成5 : 用户 用户注册界面 1 1 控制层 1 1 信息保护层,编号重复、空格乱插;有的图层叫“信息保护层”,有的叫“信息保存层”,还有的叫“数据库”。
原因:十二张图分多次画,每次凭手感命名,没有统一模板。
解决:先定一份对象名表,界面层统一叫“XX界面”,控制层统一叫“控制层”,持久层统一叫“信息保护层”,然后所有图照着抄。编号从 1 开始连续递增,同一个消息跨层回传时允许复用编号但要用返回箭头区分。这类一致性问题不影响功能,但评审时会被反复追问“这两层是不是一回事”,很消耗时间。
5.5 经理角色悬空、订单状态机缺失
现象:需求模型里出现“经理”角色,但十二个用例没有一个是它的;订单有五种状态名,但没有一张图说明状态之间怎么迁移。
原因:角色是在早期讨论时加的,后来功能砍了但角色忘了删;状态名是写在事件流里的散句,没有汇总成图。
解决:角色要么挂用例要么删,别留中间态;状态流转单独画一张状态图,标清每条迁移的触发方和触发条件。状态图这东西画一次能用整个项目周期,比在代码里写十几个 if 稳得多。
6. 二次开发:把文档变成能校验的模型资产
拿到这类建模文档,最怕的是直接照着写代码,写完发现类图和用例对不上。我的做法是先做一次“模型可执行化”改造:把类图转成建表语句,把用例的事件流转成接口清单,两边一碰,缺口立刻显形。
-- 从类图直接映射,字段类型按第五章的结论修正 CREATE TABLE dish ( dish_id BIGINT PRIMARY KEY AUTO_INCREMENT, dish_name VARCHAR(64) NOT NULL, dish_price DECIMAL(10,2) NOT NULL, -- 网订价 mark_price DECIMAL(10,2), -- 市场价, 用于划线展示 is_meat TINYINT(1) DEFAULT 0, cooking VARCHAR(32), material VARCHAR(128), description TEXT, image_url VARCHAR(255), -- 补上检索用例要求的图片 is_hot TINYINT(1) DEFAULT 0, is_recommend TINYINT(1) DEFAULT 0 ); CREATE TABLE order_item ( item_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) 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, INDEX idx_order_no (order_no) );order_item里冗余dish_name和unit_price是为了兑现“删除餐品后订单记录仍保留”这条需求;image_url是补类图缺失;DECIMAL(10,2)是第五章那三条类型修正的落地。建完表拿用例清单过一遍,哪个用例找不到对应字段,就是建模时漏的。
订单状态流转我一般单独写一个校验函数,把手写的 if 换成显式迁移表:
# 状态迁移白名单: (当前状态, 动作) -> 目标状态 TRANSITIONS = { ("PENDING", "SHIP"): "SHIPPED", ("PENDING", "CANCEL"): "CANCELLED", ("SHIPPED", "CONFIRM"): "COMPLETED", } def transit(current, action): target = TRANSITIONS.get((current, action)) if target is None: raise ValueError(f"非法流转: {current} -/-> {action}") return targetTRANSITIONS就是第四章那张迁移表的代码版,字典的键是二元组,查不到就是非法操作,已发订单想撤销会直接抛错,不依赖前端置灰。上线前把这张表对着文档备选流逐条核一遍,能省掉后期大量“为什么这个单能撤”的排查。
从那以后我每次接手这类只有图没有代码的建模文档,都会先跑一遍“用例字段反查 + 状态迁移白名单”这两步,再动手写第一行接口,模型和实现对不上的坑基本就堵在前面了。希望帮到你。
本文还有配套的精品资源,点击获取