ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UML状态图设计指南:从核心概念到订单系统实战

UML状态图设计指南:从核心概念到订单系统实战

1. 状态图到底在解决什么问题

第一次接触状态图的人,多半会有个疑问:流程图我画得挺顺手的,为什么还要单独学一种图?这个问题我当年也问过自己。后来在一个订单系统的项目里踩了坑才明白——流程图擅长描述“一件事从头到尾怎么做”,但它描述不了“一个东西在不同条件下会变成什么样”。这两件事看起来像,实际上差得很远。

举个生活里的例子。你网购一件商品,从下单到收货,中间会经历“待付款”“已付款”“已发货”“运输中”“已签收”这些阶段。每个阶段里,这个订单能做的事情是不一样的:待付款时你能取消,已发货后你就只能等或者申请退货。这种“对象在生命周期中经历的各种状态,以及状态之间怎么切换”的描述工具,就是状态图。

状态图(State Diagram),在UML体系里也叫状态机图(State Machine Diagram)。它专门用来刻画一个对象或系统在生命周期内的动态行为。核心就三样东西:状态、事件、转移。状态是对象所处的某种情形,事件是触发变化的外部刺激,转移是从一个状态跳到另一个状态的路径。听起来简单,但真正把它用对、用好,里面有不少门道。

这篇文章适合谁看?如果你正在做系统设计、写需求文档、梳理业务规则,或者准备软考、学UML,那状态图是你绕不开的一环。我会从设计思路讲到实操细节,再把我自己踩过的坑和排查经验都摊开说,尽量让你看完就能上手画出一张靠谱的状态图。

2. 状态图的核心概念与设计思路拆解

2.1 状态图和其他UML图的本质区别

很多人学UML时会把状态图和活动图搞混,因为它们都涉及“流程”和“箭头”。我一开始也分不清,直到有位前辈点了我一句:活动图描述的是“动作的流转”,状态图描述的是“对象的变迁”。

打个比方。活动图像是一份菜谱,告诉你先切菜、再热油、再下锅,每一步是一个动作。状态图则像是一个温度计的刻度,水从“常温”到“加热中”再到“沸腾”,每一步是一种状态。菜谱关心的是“做什么”,状态图关心的是“处于什么情况”。

这个区别直接决定了它们的应用场景。活动图适合描述业务流程、算法步骤、用例内部的执行逻辑。状态图适合描述订单状态、用户账号状态、设备运行状态、工单流转状态这类有明确生命周期的东西。

还有一个容易混淆的是时序图。时序图强调的是对象之间按时间顺序的消息交互,它关注的是“谁在什么时候给谁发了什么消息”。状态图关注的是“单个对象自己内部的状态怎么变”。一个向外看,一个向内看。

2.2 状态图四大核心元素拆解

状态图的基本元素不多,但每一个都有讲究。我按重要性排个序,逐个说。

状态(State)是状态图的骨架。一个状态代表对象在某个时间段内所处的稳定情形。比如“已登录”“待审核”“运行中”。状态的命名有个原则:用形容词或进行时态,描述一种持续的情况,而不是一个瞬间动作。写“审核中”比写“审核”好,写“已锁定”比写“锁定”好。这个细节很多人不注意,但命名不规范会让整张图读起来别扭。

状态还分简单状态和复合状态。简单状态就是不能再拆的,复合状态里面还嵌套着子状态机。比如“运行中”这个状态内部可能还分“正常运转”和“降速运转”。复合状态在复杂系统里很常见,但新手容易滥用,把图搞得像俄罗斯套娃。我的建议是:能拆成独立图就拆,别硬塞在一张图里。

转移(Transition)是两个状态之间的箭头,表示状态切换。转移上可以标注三样东西:事件、守卫条件、动作。格式是“事件[守卫条件]/动作”。比如“提交订单[金额>0]/生成支付单”。事件是触发原因,守卫条件是必须满足的布尔表达式,动作是切换时执行的操作。这三样不是必须都写,但写全了信息量会大很多。

事件(Event)是触发转移的外部刺激。常见的有信号事件、调用事件、时间事件、变化事件。时间事件用“after(5秒)”或“when(时间点)”表示,变化事件用“when(条件)”表示。我见过有人把所有触发都写成信号事件,结果时间相关的逻辑表达不出来,图就失真了。

初始状态和终止状态是两个特殊节点。初始状态用实心黑圆表示,一个状态图有且只有一个。终止状态用带圈的实心黑圆表示,可以有多个,也可以没有。这里有个坑:终止状态不是必须的。如果一个对象的状态是循环的、永不结束的,比如一个常驻服务的运行状态,那就不需要终止状态。硬加一个反而画蛇添足。

2.3 为什么状态图值得单独学

有人会问,我用文字描述状态不行吗?非要画图?我的经验是:文字描述状态在超过五个状态后就会失控。

我做过一个实验。一个六状态的工单系统,用文字写清楚所有状态和转移条件,写了将近八百字,读的人还是容易漏掉边界情况。画成状态图后,一张A4纸搞定,而且一眼就能看出哪些状态是孤立的、哪些转移缺少条件、哪些状态没有出口。图的价值在于它强迫你把所有可能性都摆出来,藏不住。

状态图还有个隐藏价值:它是测试用例的天然来源。状态覆盖、转移覆盖、路径覆盖,这些测试策略直接对应状态图上的元素。一张完整的状态图,基本就是一份测试大纲。我在做测试设计时,经常直接拿状态图来推导用例,效率比凭空想高得多。

3. 状态图实操:从零画出一张能用的图

3.1 画图前的准备工作

动手画之前,先别急着打开工具。我踩过的最大坑就是上来就画,画到一半发现概念没理清,推翻重来。正确的顺序应该是这样:

第一步,明确对象边界。你要描述的是哪个对象?订单、用户、设备还是整个系统?边界不清,状态就会混。比如“订单状态”和“支付状态”是两个不同的对象,硬画在一张图里会乱。我一般会先写一句话:“这张图描述的是____在____过程中的状态变化。”

第二步,穷举所有状态。拿张纸,把你能想到的状态全列出来,先不管对不对。这一步要的是全,不是准。列完之后再合并同类的、删掉冗余的。

第三步,确定初始状态和可能的终止状态。初始状态通常只有一个,终止状态看业务需要。

第四步,梳理事件和转移。对每个状态问三个问题:什么事件能让它离开?离开后去哪?离开时需要满足什么条件?

第五步,检查完整性。有没有状态是进不去的?有没有状态是出不来的?有没有事件没有对应的转移?这三个问题能筛掉大部分低级错误。

3.2 状态识别的常见误区与纠正

状态识别是状态图里最容易出错的地方。我总结了几个高频误区,你看看有没有中招。

误区一:把动作当状态。“发送邮件”是动作,“邮件已发送”才是状态。判断标准很简单:状态能持续一段时间,动作是瞬间完成的。如果你发现某个“状态”停留时间为零,那它多半是个动作,应该放在转移上作为动作标注。

误区二:状态粒度过细。有人把“输入用户名”“输入密码”“点击登录”都当成状态,结果图画了几十个框。状态应该描述有业务意义的稳定情形,不是操作步骤。登录过程可以简化为“未登录”和“已登录”两个状态,中间过程用转移上的动作表达。

误区三:状态粒度过粗。反过来,把所有情况都塞进“处理中”一个状态,那状态图就失去了意义。粒度的把握原则是:如果一个状态内部的行为差异大到需要分别描述,那就该拆。

误区四:遗漏异常状态。只画正常流程,不画“失败”“超时”“取消”这些异常状态,是新手通病。但恰恰是这些异常状态,在开发和测试时最容易出问题。我的习惯是:每画一个正常状态,就问一句“如果这里出错了会怎样”。

3.3 转移与事件的标注规范

转移上的标注是状态图信息密度最高的地方,也是最容易写乱的地方。标准格式是:

事件名 [守卫条件] / 动作

三个部分用空格和斜杠分隔。我逐个说下写法。

事件名用动词或动词短语,比如“提交”“超时”“收到确认”。守卫条件用方括号包起来,是一个布尔表达式,比如“[余额充足]”“[重试次数<3]”。动作用斜杠开头,描述状态切换时执行的操作,比如“/扣减库存”“/发送通知”。

有个细节:守卫条件和事件的关系是“与”。也就是说,事件发生了,并且守卫条件为真,转移才会触发。如果守卫条件为假,事件被忽略,对象停留在原状态。这个语义很多人理解错,以为守卫条件是“或”的关系。

还有,一个事件可以触发多条转移,只要守卫条件互斥就行。比如“提交订单”事件,在“[金额>0]”时转到“待支付”,在“[金额<=0]”时转到“无效订单”。这种写法比画两个事件更准确,因为触发原因确实是同一个。

3.4 复合状态与区域的使用时机

复合状态是把一组相关的子状态打包成一个状态。什么时候该用?我的判断标准是:当多个状态共享同一组转移时。

举个例子。一个播放器有“播放中”“暂停中”“停止中”三个状态,它们都能响应“切换曲目”事件,都会转到“加载中”。如果不打包,每个状态都要画一条到“加载中”的线,图会很乱。打包成“播放器活动”复合状态后,只需要从复合状态画一条线到“加载中”,简洁很多。

区域(Region)是复合状态内部可以并行执行的子状态机。比如一个游戏角色,同时有“移动状态”(静止/行走/奔跑)和“战斗状态”(和平/攻击/防御),这两组状态是独立的,可以并行。用区域就能表达这种并行关系。

但我要提醒一句:复合状态和区域是状态图里最容易被滥用的特性。新手看到能嵌套就拼命嵌套,结果图画得像迷宫。我的原则是:能用简单状态解决就不用复合状态,能拆成两张图就不塞进一张图。图是给人看的,不是炫技的。

4. 完整实操案例:一个订单状态图的诞生

4.1 需求梳理与状态清单

假设我们要为一个电商订单系统设计状态图。先别画,先把需求理清楚。

订单从创建到结束,会经历哪些阶段?我列一下:

  • 刚创建,还没付款:待支付
  • 用户付了款:已支付
  • 商家发货了:已发货
  • 用户收到了:已签收
  • 用户申请退款:退款中
  • 退款完成:已退款
  • 用户取消:已取消
  • 超时未付款:已关闭

这是正常和异常混在一起的清单。接下来要合并和整理。比如“已签收”之后如果还有评价流程,那可以加“已完成”。但为了聚焦,我们先按上面这些来。

初始状态是“待支付”,终止状态有“已完成”“已取消”“已关闭”“已退款”几个。

4.2 事件与转移条件梳理

状态列完了,接下来梳理事件。对每个状态问:什么事件能让它离开?

待支付状态下,可能的事件有:用户付款、用户取消、超时。对应转移:

  • 付款[支付成功] → 已支付
  • 取消 → 已取消
  • after(30分钟) → 已关闭

已支付状态下:

  • 商家发货 → 已发货
  • 用户申请退款 → 退款中

已发货状态下:

  • 用户确认收货 → 已签收
  • 用户申请退款 → 退款中

已签收状态下:

  • 用户申请退款 → 退款中
  • 用户评价 → 已完成

退款中状态下:

  • 退款成功 → 已退款
  • 退款失败 → 回到原状态(这里要注意,退款失败应该回到申请退款前的状态,但状态图里没法直接表达“回到之前的状态”,需要根据业务决定是回到已支付还是已发货。这是个典型的建模难点。)

这个“退款失败回到哪”的问题,我在实际项目里遇到过。解决方案有两种:一是退款失败后统一回到“已支付”,让商家重新处理;二是引入一个“退款失败”状态,人工介入。具体选哪种,取决于业务规则,没有标准答案。状态图的价值就在于把这个模糊点暴露出来,逼着产品和技术一起确认。

4.3 绘图工具选择与操作要点

工具这块,我用过不少。早期用Visio,后来用Draw.io,再后来用PlantUML和Mermaid。各有优劣,我做个对比。

工具优点缺点适用场景
Draw.io免费、在线、拖拽方便复杂图排版费劲快速画图、团队协作
PlantUML文本描述、版本可控学习成本、样式受限代码化建模、文档集成
Visio功能全、模板多收费、笨重企业级文档
手绘快、灵活不好保存和修改头脑风暴、讨论

我现在的习惯是:讨论阶段手绘或Draw.io,定稿后用PlantUML写成文本,方便进版本库。PlantUML画状态图的语法很简单:

@startuml [*] --> 待支付 待支付 --> 已支付 : 付款[支付成功] 待支付 --> 已取消 : 取消 待支付 --> 已关闭 : after(30分钟) 已支付 --> 已发货 : 商家发货 已支付 --> 退款中 : 申请退款 已发货 --> 已签收 : 确认收货 已发货 --> 退款中 : 申请退款 已签收 --> 已完成 : 评价 已签收 --> 退款中 : 申请退款 退款中 --> 已退款 : 退款成功 退款中 --> 已支付 : 退款失败 已退款 --> [*] 已取消 --> [*] 已关闭 --> [*] 已完成 --> [*] @enduml

这段代码直接贴到支持PlantUML的工具里就能出图。文本化的好处是改起来快,而且能进Git做版本管理。团队里谁改了哪个状态,diff一看就清楚。

4.4 图的验证与评审要点

图画完了不代表结束,还得验证。我一般用这几个检查项过一遍:

可达性检查:每个状态是不是都能从初始状态到达?有没有孤立的状态?上面例子里,如果“已退款”只能从“退款中”到达,而“退款中”能从多个状态进入,那就是可达的。

出口检查:除了终止状态,每个状态是不是都有出口?有没有“死状态”——进去了出不来?这个在复杂图里很常见,尤其是异常处理分支。

事件完整性:每个事件是不是都有对应的处理?比如“申请退款”事件,在“已支付”“已发货”“已签收”三个状态下都能触发,那这三个状态都要有对应的转移。漏掉一个,开发时就会出bug。

条件互斥性:同一个事件的多条转移,守卫条件是不是互斥的?如果两个条件可能同时为真,那就有歧义,需要合并或调整。

命名一致性:状态名、事件名是不是统一风格?别一会儿“待支付”一会儿“等待付款”,读起来累。

评审的时候,我建议拉上产品、开发、测试三方一起看。产品确认业务逻辑对不对,开发确认可实现性,测试确认覆盖度。一张状态图评审下来,能提前发现不少需求漏洞。

5. 常见问题与排查技巧实录

5.1 状态图与流程图的边界怎么划

这是被问得最多的问题。我的回答是:看你要描述的是“一件事”还是“一个东西”。

如果你要描述的是“用户下单这个流程”,那用活动图或流程图。如果你要描述的是“订单这个对象的状态变化”,那用状态图。前者关注动作序列,后者关注对象情形。

但实际项目里,两者经常配合使用。比如用活动图描述“用户下单”的流程,其中“订单创建”这一步可以链接到订单状态图,说明订单创建后进入什么状态。这种组合用法比单用一种图更清晰。

还有个判断技巧:如果图里的节点能用“正在做某事”来描述,那可能是活动图;如果能用“处于某种情况”来描述,那可能是状态图。“正在支付”是活动,“待支付”是状态。这个语感需要多练。

5.2 状态爆炸怎么破

状态爆炸是状态图设计里的经典难题。当对象有多个独立属性时,状态数会呈指数增长。比如一个订单有“支付状态”(未付/已付)和“发货状态”(未发/已发),组合起来就是四个状态。如果再加一个“退款状态”,就变成八个。

破解方法有几个:

方法一:拆分对象。把不同维度的状态拆到不同对象上。支付状态属于“支付单”,发货状态属于“发货单”,各自画图。这是最彻底的办法。

方法二:使用复合状态和区域。把独立维度用并行区域表达,避免组合爆炸。但前面说过,区域别滥用。

方法三:状态压缩。把不关心的组合合并。比如“未付且未发”和“未付且已发”在业务上不可能同时存在,那就不用画。

我的经验是:优先用方法一,实在不行用方法二,方法三作为补充。拆分对象虽然多画几张图,但每张图都简单清晰,维护成本低。

5.3 异常状态遗漏的排查方法

异常状态遗漏是状态图最常见的质量问题。我有个笨办法但很有效:对每个状态,逐个问“如果这里出错了会怎样”。

具体操作是拿一张状态清单,对每个状态列出可能的异常:超时、失败、取消、拒绝、中断。然后看这些异常在图里有没有对应的状态和转移。没有的就补上。

比如“已支付”状态,异常可能有:支付回调失败、库存不足、风控拦截。这些异常分别转到什么状态?如果业务上没有定义,那就是需求漏洞,得补。

这个方法我称为“异常扫描法”,虽然机械,但能覆盖大部分遗漏。我在几个项目里用下来,平均每张图能扫出三到五个遗漏的异常分支。

5.4 状态图维护与版本管理

状态图不是画完就完了,它会随着需求变化而修改。维护不好,图就会和代码脱节,最后没人看。

我的做法是:状态图进版本库,和代码一起管理。用PlantUML这类文本化工具,图就是代码,改动能追踪,能review。每次需求变更,先改图,再改代码,图是源头。

还有个技巧:在状态图里标注版本和修改记录。比如在图下方加个注释,写明“v1.2 新增退款失败分支”。这样看历史版本时能快速定位变化。

另外,状态图要和接口文档、数据库状态字段保持一致。我见过状态图写“已支付”,数据库字段存的是“PAID”,接口返回的是“paid”,三处不一致,排查问题时很痛苦。统一命名规范,能省很多事。

5.5 常见问题速查表

问题现象可能原因排查方向解决建议
状态进不去缺少入边检查是否有转移指向该状态补充转移或删除孤立状态
状态出不来缺少出边检查是否有转移从该状态出发补充出口或标记为终止状态
事件无响应守卫条件永假检查条件表达式逻辑修正条件或调整业务规则
转移歧义守卫条件重叠检查多条转移的条件是否互斥合并转移或细化条件
图太乱状态粒度过细检查是否有可合并的状态合并同类状态或拆分对象
图太简状态粒度过粗检查是否有状态内部行为差异大拆分状态或引入复合状态
与代码不符图未同步更新对比图和代码的状态定义建立图代码同步机制

这张表我放在项目wiki里,新人遇到问题先查表,能解决大部分常见疑惑。

6. 状态图在实际项目中的进阶用法

6.1 状态图驱动开发与测试

状态图不只是设计工具,它可以直接驱动开发和测试。我试过一种做法:把状态图作为开发的唯一状态来源。

具体流程是:产品画状态图,评审通过后,开发根据图实现状态机,测试根据图设计用例。图上的每个状态对应代码里的一个枚举值,每条转移对应一个状态转换方法,每个守卫条件对应一个if判断。这样图和代码是一一对应的,改图就是改需求,改代码就是实现需求。

测试用例的推导更直接。状态覆盖:每个状态至少测一次。转移覆盖:每条转移至少走一次。路径覆盖:从初始到终止的所有路径都走一遍。用状态图推导用例,覆盖率有保障,而且不容易漏。

我在一个工单系统里用过这套方法,测试用例数量比之前凭经验写的多了三成,但线上状态相关的bug少了将近一半。投入产出比很划算。

6.2 状态图与领域驱动设计的结合

如果你在做领域驱动设计(DDD),状态图是聚合根建模的好帮手。聚合根通常有明确的生命周期,状态图正好描述这个生命周期。

比如一个“订单”聚合根,它的状态变化就是业务规则的核心。把状态图画出来,聚合根的方法就呼之欲出了:每个转移对应一个领域方法,每个守卫条件对应一个业务规则校验。状态图成了领域模型的骨架。

DDD里还有个概念叫“不变量”,就是状态变化时必须保持的约束。状态图上的守卫条件,很多就是不变量的体现。比如“订单金额不能为负”这个不变量,在“提交订单”转移的守卫条件里写“[金额>0]”,就把约束显式化了。

6.3 状态图在微服务架构中的应用

微服务架构下,状态图的价值更突出。因为服务拆分后,每个服务负责一个或几个对象的状态管理,状态边界必须清晰。

我参与过一个项目,订单服务、支付服务、物流服务各自维护自己的状态。一开始状态定义混乱,订单服务认为“已支付”是终态,支付服务认为“已支付”只是中间态,两边对不上,联调时各种问题。

后来我们做了件事:为每个服务画状态图,然后对齐跨服务的状态映射。订单的“已支付”对应支付的“支付成功”,订单的“已发货”对应物流的“已揽收”。映射关系明确后,服务间的状态同步就顺畅了。

状态图在这里起的作用是契约。它明确了每个服务的状态职责边界,避免了状态定义的歧义。在微服务这种分布式环境里,这种显式契约比口头约定靠谱得多。

6.4 状态图的局限性与替代方案

说了这么多状态图的好,也得说说它的局限。

局限一:不适合描述并发。状态图本质上是单对象的状态变迁,虽然可以用区域表达并行,但表达力有限。如果系统里有大量并发交互,状态图会力不从心,这时候可能需要Petri网之类的工具。

局限二:不适合描述数据流。状态图关注控制流,不关注数据怎么流动。如果重点是数据转换,那数据流图更合适。

局限三:大规模系统难以用一张图表达。状态多了之后,图会变得庞大难读。这时候需要分层,用多张图分别描述不同层次的状态。

替代方案方面,状态机代码库(如Spring StateMachine)可以直接用代码定义状态机,省去画图环节。但我的观点是:图的价值在于沟通,代码的价值在于执行。两者不冲突,图用于设计和评审,代码用于实现。跳过图直接写代码,沟通成本会转嫁到后期。

7. 我踩过的坑与实操心得

7.1 那些年画错的状态图

我画状态图踩过的坑,说出来你可能也遇到过。

坑一:把状态图画成了流程图。早期我画订单状态图,把“用户点击下单”“系统生成订单号”“写入数据库”都画成了状态。结果图有二十多个框,读的人一脸懵。后来才明白,这些是动作,不是状态。状态应该是“待支付”“已支付”这种。

坑二:忘了画异常分支。第一版订单状态图只画了正常流程,评审时测试同学问了一句“支付超时怎么办”,我才发现漏了。后来补了“已关闭”状态和超时转移。这个教训让我养成了“每个状态问异常”的习惯。

坑三:守卫条件写成了动作。我写过“[扣减库存]”这样的守卫条件,被前辈指出这是动作不是条件。守卫条件应该是布尔表达式,比如“[库存充足]”,扣减库存是转移上的动作,写在斜杠后面。

坑四:状态命名不统一。一张图里既有“待支付”又有“等待付款”,既有“已发货”又有“发货完成”。读起来费劲,后来统一了命名规范:状态用“已/待/中”开头,事件用动词,动作用“/动词”。

7.2 评审时最容易被挑刺的地方

状态图评审时,有几个地方特别容易被挑刺,提前注意能省不少返工。

第一,初始状态不唯一。有人画了多个初始状态,评审时必被问“到底从哪开始”。除非是并行区域,否则初始状态只能有一个。

第二,终止状态缺失或过多。该有的终止状态没有,评审时会被问“这个流程怎么结束”。不该有的硬加,会被问“这个状态真的会终止吗”。

第三,事件命名模糊。“处理”“操作”这种词最容易被挑,因为不明确。事件名要具体到能让人知道发生了什么。

第四,缺少守卫条件。同一个事件有多条转移但没写条件,评审时必被问“怎么区分走哪条”。守卫条件是消除歧义的关键。

第五,状态粒度和业务不匹配。太细会被说“这是实现细节”,太粗会被说“业务没表达清楚”。粒度问题没有标准答案,但评审时一定要能说清楚为什么这么分。

7.3 提高效率的实用技巧

最后分享几个我总结的提效技巧。

技巧一:先写状态清单再画图。别上来就拖框,先用文字把状态和事件列出来,理清了再画。我一般用表格列,状态一行,事件一列,交叉处填转移条件。表格填完,图基本就出来了。

技巧二:用颜色区分状态类型。正常状态一种颜色,异常状态一种颜色,终止状态一种颜色。视觉上区分开,读图效率高很多。Draw.io和PlantUML都支持自定义颜色。

技巧三:给复杂转移加注释。有些转移的业务含义不直观,加个注释说明。比如“退款失败回到已支付”这条,注释写“退款失败后由商家重新处理”。注释不嫌多,读图的人会感谢你。

技巧四:定期回顾和更新。状态图不是一次性的,需求变了图要跟着变。我一般每个迭代回顾一次状态图,看有没有和代码脱节。脱节了及时修,别攒着。

技巧五:用状态图做新人培训材料。新人入职,让他先读状态图,比读代码快得多。状态图是业务的浓缩,读懂了图,业务逻辑就懂了七八成。我在几个团队推过这个做法,新人上手速度明显加快。

状态图这个东西,入门容易精通难。概念就那几个,但真正画好、用好,需要在项目里反复练。我的建议是:找一个你熟悉的业务对象,比如外卖订单、快递包裹、会员账号,试着画一张状态图。画完找个人看看,问他能不能看懂。能看懂,说明你画对了;看不懂,问他哪里卡住,那就是你需要改进的地方。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进