system column十三Tech
← 返回业财专栏
ERP

业财通识27:状态机设计——用状态驱动业务流转

2026/3/2116 min read
ERP状态机产品设计业务流程十三Tech

大家好,我是十三。

开篇:模块划分之后,谁来驱动流转

在上一篇关于模块划分的讨论中,我们把 ERP 系统拆解成了采购、销售、库存、财务等功能模块。模块边界清晰了,但模块内部还面临一个更基础的问题:一张单据从诞生到终结,中间会经历多少种"身份"?谁来控制它什么时候能变成下一个身份?

回顾本系列的前置文章,在第 1 篇中,我们追踪过一张采购申请(PR)如何演变为具有法律效力的采购订单(PO)。在第 8 篇中,我们也看过销售机会如何一步步转化为销售订单(SO)。这些流转的本质,都是状态的变迁。而驱动这些变迁的底层机制,就是今天的主角——状态机

状态机解决什么问题

业务定义:状态机(State Machine)是一种数学模型,用于描述对象在其生命周期内所有可能的状态,以及在何种条件下可以从一个状态转移到另一个状态。

用通俗的类比来说,状态机就像交通信号灯。红灯停、绿灯行、黄灯等待,每种灯光就是一个"状态",而时间或传感器信号就是触发状态切换的"事件"。没有信号灯的路口,车辆各行其是,必然混乱;没有状态机的单据,业务操作也必然失去约束。

在 ERP 系统中,每一张单据都有自己的生命周期。没有状态管理,单据就像一张没有签名的合同,任何人都可以随时修改,系统也无法判断下一步该做什么。

状态机解决了三个核心问题:

  1. 规范流转顺序:明确规定哪些操作可以在当前状态下执行,防止业务乱序。未审批的订单不能直接收货,未发货的订单不能开票。
  2. 明确权责边界:每个状态变化都对应特定的操作人和操作权限,出了问题可以追溯。
  3. 驱动后续动作:状态变更往往是连锁反应的触发器。订单变为"已审批"后,系统才能通知仓库备货;变为"完全收货"后,财务才能启动对账。

采购订单状态机示例

我们以采购订单(PO)为例,这是最典型的单据状态机之一。在第 1 篇中,我们介绍过 PO 从创建到发送给供应商的过程。那张订单在系统内部,其实经历了一个更精细的状态迁移。

stateDiagram
    [*] --> 草稿
    草稿 --> 待审批: 提交审批
    待审批 --> 已审批: 审批通过
    待审批 --> 已驳回: 审批拒绝
    已驳回 --> 草稿: 修改后重提
    已审批 --> 部分收货: 部分到货
    已审批 --> 完全收货: 全部到货
    部分收货 --> 完全收货: 剩余到货
    完全收货 --> 已关闭: 结算完成
    草稿 --> 已取消: 主动取消
    待审批 --> 已取消: 撤销申请
    已审批 --> 已冻结: 异常触发
    部分收货 --> 已冻结: 异常触发
    已冻结 --> 已审批: 异常解除

下面逐个说明每个状态的业务含义。

草稿:采购员刚创建订单,尚未提交。此时可以任意修改商品、数量、价格,甚至可以删除整张订单。草稿状态是订单的"sandbox",所有操作都不产生外部影响。

待审批:采购员确认无误后提交,订单进入审批队列。此时订单内容被锁定,不允许修改,等待主管或财务审批。这个状态是内部控制的第一个关键节点。

已审批:审批通过,订单正式生效。系统可以据此通知供应商,仓库也可以开始准备收货。从这一刻起,订单对外产生了约束力。

部分收货:供应商分批送货,仓库已收到一部分但尚未收齐。此时订单处于履约中,仍允许后续到货。部分收货是制造业和批发业最常见的场景。

完全收货:所有商品已按订单要求送达并入库。这是履约完成的标志,财务可以启动对账和付款流程。

已关闭:订单对应的账务已结清,或订单被正常终止。这是采购订单生命周期的终点,状态不再变更。

以上是 6 个正常流转状态。此外,还有 3 个异常或终结状态:

已驳回:审批人认为订单有问题(如价格过高、供应商不合规),将订单退回。采购员需要修改后重新提交。

已取消:在订单生效前,业务部门因需求变更主动取消。一旦进入"已审批"状态,通常不允许直接取消,而需要通过关闭流程处理。

已冻结:在履约过程中发现异常(如货物质量争议、供应商失信),临时暂停订单执行。冻结期间,不允许继续收货或付款。

状态迁移规则表

状态机不是画一张图就够了。产品设计中,必须有一张规则表来定义每个迁移的触发条件、前置校验和副作用。

当前状态 触发事件 目标状态 前置条件
草稿 提交审批 待审批 订单内容完整,金额大于 0
待审批 审批通过 已审批 审批人权限足够,预算充足
待审批 审批拒绝 已驳回 审批人填写拒绝原因
已驳回 修改重提 草稿 采购员修改了被驳回的字段
已审批 部分到货 部分收货 仓库确认收货数量小于订单数量
已审批 全部到货 完全收货 仓库确认收货数量等于订单数量
部分收货 剩余到货 完全收货 累计收货数量等于订单数量
完全收货 结算完成 已关闭 财务完成对账并付款
草稿 主动取消 已取消 订单尚未提交
待审批 撤销申请 已取消 申请人本人或上级撤销
已审批 异常触发 已冻结 质量异常或供应商风险
已冻结 异常解除 已审批 风险消除,责任人确认

这张表是开发和测试的基准。每一个"前置条件"背后,都对应着一段校验逻辑。例如"预算充足"需要检查部门年度预算余额是否大于订单金额;"审批人权限足够"需要检查审批人的职级或授权范围是否覆盖该订单金额。

副作用也是规则表的重要组成部分,虽然表中未单独列出一列,但在实际设计中必须考虑:

  • 草稿 → 待审批:锁定订单字段,禁止编辑,生成审批任务。
  • 待审批 → 已审批:生成供应商通知,触发库存预占,扣减部门预算余额。
  • 已审批 → 部分收货:更新库存数量,生成入库单,记录应付暂估。
  • 完全收货 → 已关闭:生成应付账款凭证,释放预算占用,归档订单。

异常状态处理

真实业务从不按理想路径走。异常状态处理是状态机设计中最容易遗漏、却也最关键的部分。

取消

取消是最常见的异常路径,但不是所有状态都允许取消。

  • 草稿状态:随时可取消,无成本,直接删除或标记即可。
  • 待审批状态:申请人可以撤销,但需要记录撤销原因,便于后续审计。
  • 已审批状态:原则上不允许直接取消。因为订单已经对外生效,取消等同于违约。正确的做法是发起"订单关闭"流程,与供应商协商一致后,通过关闭路径终止。

为什么已审批后不能直接取消?因为状态变更的不可逆性,是保障业务流程严肃性的核心。如果已审批的订单可以随意取消,供应商可能已经开始备货,仓库可能已经在等待收货,随意取消会造成协作混乱和经济损失。

驳回

驳回与取消不同。取消是申请人主动放弃,驳回是审批人拒绝通过。

在第 9 篇中,我们讨论过信用检查的审批逻辑。采购审批的道理相同:当订单金额超出审批人权限、或预算不足、或供应商资质有问题时,审批人选择驳回。

驳回的设计要点有三条:

  1. 必须填写驳回原因,否则采购员不知道改什么。
  2. 驳回后订单回退到草稿状态,而不是直接删除。采购员修改后可以重新提交,保留历史记录。
  3. 多次驳回需要升级提醒。如果一张订单被驳回三次,应该自动通知更高层级的管理者介入。

异常冻结

冻结是一种临时状态,用于处理履约过程中的突发事件。

典型触发场景包括:仓库收货时发现货物质量不合格,需要暂停付款;供应商被曝出财务危机,需要暂停后续到货;市场价格剧烈波动,需要重新评估订单合理性。

冻结的设计原则:

  1. 冻结必须指定冻结类型和预计解冻时间,防止无限期冻结。
  2. 冻结期间,与订单相关的下游操作(收货、付款)全部暂停。
  3. 解冻时需要责任人确认,而不是自动恢复。确认解冻意味着风险已经消除,业务可以继续。

超时自动关闭

超时关闭是防止"僵尸订单"占用系统资源的机制。

不同状态的超时策略不同:

状态 超时时间 超时动作
草稿 7 天未提交 自动关闭
待审批 3 天未审批 提醒审批人,5 天后升级
已审批 30 天未收货 提醒采购员,60 天后冻结

超时关闭不是简单的删除,而是状态迁移到"已关闭",并记录关闭原因(超时),同时释放相关资源(如预算占用)。

状态回退

状态回退是状态机设计中最具争议的话题。严格来说,理想的状态机不允许回退——就像时间不能倒流。但在实际业务中,回退不可避免。

什么情况下允许回退

  1. 审批驳回:已提交的订单被审批人拒绝,需要从待审批回退到草稿。这是最常见的回退场景。
  2. 异常解冻:已冻结的订单经调查后确认无风险,需要从冻结回退到上一状态。
  3. 操作撤销:操作人员在短时间内(如 5 分钟内)发现自己操作错误,申请撤销。

回退的风险

允许回退会带来三个核心风险:

  1. 数据一致性破坏:如果"已审批"回退到"草稿",但供应商已经收到通知并开始备货,系统状态与实际情况就产生了不一致。
  2. 连锁反应复杂:一个订单状态回退,可能触发下游一系列动作的撤销。例如,已审批状态触发了库存预占,回退时需要释放预占;如果仓库已经开始拣货,回退会变得更加复杂。
  3. 审计追踪困难:频繁回退会让单据的历史轨迹变得混乱,审计时难以判断哪些操作是合理的、哪些是违规的。

回退的处理方式

我的建议是:用"正向流转"替代"物理回退"

与其让状态真的退回去,不如定义一个新的正向状态来表达"回退"的语义。例如:

// 不推荐:物理回退
已审批 --撤销--> 草稿

// 推荐:正向流转到新状态
已审批 --撤销--> 已撤销(待修改)
已撤销 --修改完成--> 待审批

这样做的好处是:状态迁移始终是单向的,历史记录清晰,不会出现循环。而且"已撤销"作为一个独立状态,可以附加自己的校验规则,如撤销必须在 24 小时内申请。

以下是回退判断的伪代码逻辑:

function canTransition(currentState, event, operator) {
  // 1. 检查状态迁移是否被定义
  const transition = TRANSITION_TABLE[currentState]?.[event];
  if (!transition) return { ok: false, reason: "当前状态不支持此操作" };

  // 2. 检查操作人权限
  if (!hasPermission(operator, transition.requiredRole)) {
    return { ok: false, reason: "权限不足" };
  }

  // 3. 执行前置校验
  const check = transition.precondition(order);
  if (!check.ok) return { ok: false, reason: check.reason };

  // 4. 回退特殊检查:是否已产生下游单据
  if (transition.isRollback && hasDownstreamDocs(order)) {
    return { ok: false, reason: "已产生下游单据,不允许回退" };
  }

  return { ok: true, targetState: transition.target };
}

这段代码的核心在于第 4 步:回退操作前,必须检查是否已经产生了下游影响。如果下游已经动起来,物理回退就必须被禁止,只能通过人工协商或补偿流程来处理。

状态机的产品设计原则

状态机设计没有标准答案,但有一些经过实践检验的原则。

状态数量控制

状态不是越多越好。每增加一个状态,开发和测试的复杂度都会成倍增长。

我的建议是:核心业务状态不超过 7 个,超过 7 个用户会难以理解和记忆。异常状态尽量复用,例如"已取消"和"已关闭"在很多场景下可以合并。不要把"子状态"和"主状态"混为一谈,"待审批"是一个状态,而"待主管审批"和"待财务审批"是子流程,应该用审批流引擎来管理,而不是状态机。

命名规范

状态命名直接影响用户理解和开发效率。好的状态名有三个特征:

  1. 动宾结构或形容词短语:如"待审批""已审批""完全收货",一看就知道当前处于什么阶段。
  2. 无二义性:避免"处理中""审核中"这种模糊词汇。是"待审批"还是"审批中"?要明确区分"等待别人做"和"正在做"。
  3. 成对出现:有"待审批"就要有"已审批",有"部分收货"就要有"完全收货",形成清晰的对比关系。

可视化需求

状态机不能只存在于代码里,产品经理和业务员都需要看到它。

产品设计中应提供三种可视化:

  1. 单据状态条:在订单详情页顶部,用进度条形式展示当前状态和已完成的状态。这是用户最直观的感知方式。
  2. 状态迁移图:在系统管理后台,用图形化方式展示所有状态及其关系。当业务规则调整时,这张图就是开发和测试的参照。
  3. 操作按钮动态显示:当前状态下允许的操作才显示按钮,不允许的操作隐藏或置灰。这比让用户点完按钮再提示"当前状态不允许"要友好得多。

总结

状态机是 ERP 系统的"交通规则"。没有它,单据就像没有信号灯的车流,各行其是,最终必然撞车。

今天我们围绕采购订单,完整拆解了状态机的六个方面:状态定义、迁移规则、异常处理、回退策略、命名规范和可视化设计。这些原则不仅适用于采购订单,也适用于销售订单、入库单、对账单等任何有生命周期的业务单据。

记住两个核心判断:

第一,状态机的本质是约束。约束带来秩序,秩序带来可预测性。不要把状态机设计得过于灵活,灵活到失去约束的意义。

第二,状态变更的副作用比状态本身更重要。一个状态从 A 变成 B,真正的业务价值往往在于 B 触发的那一连串下游动作。设计状态机时,要把一半精力放在定义状态,一半精力放在定义副作用。

下一篇,我们将进入 ERP 产品设计的另一个核心话题——权限设计。当状态机规定了单据能怎么流转,权限体系则规定了谁能推动这个流转。状态机与权限,是一对不可分割的设计搭档。


往期回顾


关于十三Tech

资深服务端研发工程师、架构师、AI 编程实践者。
专注分享真实的技术实践经验,持续记录企业系统、架构设计与 AI 编程实践。