大家好,我是十三。
开篇:模块划分之后,谁来驱动流转
在上一篇关于模块划分的讨论中,我们把 ERP 系统拆解成了采购、销售、库存、财务等功能模块。模块边界清晰了,但模块内部还面临一个更基础的问题:一张单据从诞生到终结,中间会经历多少种"身份"?谁来控制它什么时候能变成下一个身份?
回顾本系列的前置文章,在第 1 篇中,我们追踪过一张采购申请(PR)如何演变为具有法律效力的采购订单(PO)。在第 8 篇中,我们也看过销售机会如何一步步转化为销售订单(SO)。这些流转的本质,都是状态的变迁。而驱动这些变迁的底层机制,就是今天的主角——状态机。
状态机解决什么问题
业务定义:状态机(State Machine)是一种数学模型,用于描述对象在其生命周期内所有可能的状态,以及在何种条件下可以从一个状态转移到另一个状态。
用通俗的类比来说,状态机就像交通信号灯。红灯停、绿灯行、黄灯等待,每种灯光就是一个"状态",而时间或传感器信号就是触发状态切换的"事件"。没有信号灯的路口,车辆各行其是,必然混乱;没有状态机的单据,业务操作也必然失去约束。
在 ERP 系统中,每一张单据都有自己的生命周期。没有状态管理,单据就像一张没有签名的合同,任何人都可以随时修改,系统也无法判断下一步该做什么。
状态机解决了三个核心问题:
- 规范流转顺序:明确规定哪些操作可以在当前状态下执行,防止业务乱序。未审批的订单不能直接收货,未发货的订单不能开票。
- 明确权责边界:每个状态变化都对应特定的操作人和操作权限,出了问题可以追溯。
- 驱动后续动作:状态变更往往是连锁反应的触发器。订单变为"已审批"后,系统才能通知仓库备货;变为"完全收货"后,财务才能启动对账。
采购订单状态机示例
我们以采购订单(PO)为例,这是最典型的单据状态机之一。在第 1 篇中,我们介绍过 PO 从创建到发送给供应商的过程。那张订单在系统内部,其实经历了一个更精细的状态迁移。
stateDiagram
[*] --> 草稿
草稿 --> 待审批: 提交审批
待审批 --> 已审批: 审批通过
待审批 --> 已驳回: 审批拒绝
已驳回 --> 草稿: 修改后重提
已审批 --> 部分收货: 部分到货
已审批 --> 完全收货: 全部到货
部分收货 --> 完全收货: 剩余到货
完全收货 --> 已关闭: 结算完成
草稿 --> 已取消: 主动取消
待审批 --> 已取消: 撤销申请
已审批 --> 已冻结: 异常触发
部分收货 --> 已冻结: 异常触发
已冻结 --> 已审批: 异常解除
下面逐个说明每个状态的业务含义。
草稿:采购员刚创建订单,尚未提交。此时可以任意修改商品、数量、价格,甚至可以删除整张订单。草稿状态是订单的"sandbox",所有操作都不产生外部影响。
待审批:采购员确认无误后提交,订单进入审批队列。此时订单内容被锁定,不允许修改,等待主管或财务审批。这个状态是内部控制的第一个关键节点。
已审批:审批通过,订单正式生效。系统可以据此通知供应商,仓库也可以开始准备收货。从这一刻起,订单对外产生了约束力。
部分收货:供应商分批送货,仓库已收到一部分但尚未收齐。此时订单处于履约中,仍允许后续到货。部分收货是制造业和批发业最常见的场景。
完全收货:所有商品已按订单要求送达并入库。这是履约完成的标志,财务可以启动对账和付款流程。
已关闭:订单对应的账务已结清,或订单被正常终止。这是采购订单生命周期的终点,状态不再变更。
以上是 6 个正常流转状态。此外,还有 3 个异常或终结状态:
已驳回:审批人认为订单有问题(如价格过高、供应商不合规),将订单退回。采购员需要修改后重新提交。
已取消:在订单生效前,业务部门因需求变更主动取消。一旦进入"已审批"状态,通常不允许直接取消,而需要通过关闭流程处理。
已冻结:在履约过程中发现异常(如货物质量争议、供应商失信),临时暂停订单执行。冻结期间,不允许继续收货或付款。
状态迁移规则表
状态机不是画一张图就够了。产品设计中,必须有一张规则表来定义每个迁移的触发条件、前置校验和副作用。
| 当前状态 | 触发事件 | 目标状态 | 前置条件 |
|---|---|---|---|
| 草稿 | 提交审批 | 待审批 | 订单内容完整,金额大于 0 |
| 待审批 | 审批通过 | 已审批 | 审批人权限足够,预算充足 |
| 待审批 | 审批拒绝 | 已驳回 | 审批人填写拒绝原因 |
| 已驳回 | 修改重提 | 草稿 | 采购员修改了被驳回的字段 |
| 已审批 | 部分到货 | 部分收货 | 仓库确认收货数量小于订单数量 |
| 已审批 | 全部到货 | 完全收货 | 仓库确认收货数量等于订单数量 |
| 部分收货 | 剩余到货 | 完全收货 | 累计收货数量等于订单数量 |
| 完全收货 | 结算完成 | 已关闭 | 财务完成对账并付款 |
| 草稿 | 主动取消 | 已取消 | 订单尚未提交 |
| 待审批 | 撤销申请 | 已取消 | 申请人本人或上级撤销 |
| 已审批 | 异常触发 | 已冻结 | 质量异常或供应商风险 |
| 已冻结 | 异常解除 | 已审批 | 风险消除,责任人确认 |
这张表是开发和测试的基准。每一个"前置条件"背后,都对应着一段校验逻辑。例如"预算充足"需要检查部门年度预算余额是否大于订单金额;"审批人权限足够"需要检查审批人的职级或授权范围是否覆盖该订单金额。
副作用也是规则表的重要组成部分,虽然表中未单独列出一列,但在实际设计中必须考虑:
- 草稿 → 待审批:锁定订单字段,禁止编辑,生成审批任务。
- 待审批 → 已审批:生成供应商通知,触发库存预占,扣减部门预算余额。
- 已审批 → 部分收货:更新库存数量,生成入库单,记录应付暂估。
- 完全收货 → 已关闭:生成应付账款凭证,释放预算占用,归档订单。
异常状态处理
真实业务从不按理想路径走。异常状态处理是状态机设计中最容易遗漏、却也最关键的部分。
取消
取消是最常见的异常路径,但不是所有状态都允许取消。
- 草稿状态:随时可取消,无成本,直接删除或标记即可。
- 待审批状态:申请人可以撤销,但需要记录撤销原因,便于后续审计。
- 已审批状态:原则上不允许直接取消。因为订单已经对外生效,取消等同于违约。正确的做法是发起"订单关闭"流程,与供应商协商一致后,通过关闭路径终止。
为什么已审批后不能直接取消?因为状态变更的不可逆性,是保障业务流程严肃性的核心。如果已审批的订单可以随意取消,供应商可能已经开始备货,仓库可能已经在等待收货,随意取消会造成协作混乱和经济损失。
驳回
驳回与取消不同。取消是申请人主动放弃,驳回是审批人拒绝通过。
在第 9 篇中,我们讨论过信用检查的审批逻辑。采购审批的道理相同:当订单金额超出审批人权限、或预算不足、或供应商资质有问题时,审批人选择驳回。
驳回的设计要点有三条:
- 必须填写驳回原因,否则采购员不知道改什么。
- 驳回后订单回退到草稿状态,而不是直接删除。采购员修改后可以重新提交,保留历史记录。
- 多次驳回需要升级提醒。如果一张订单被驳回三次,应该自动通知更高层级的管理者介入。
异常冻结
冻结是一种临时状态,用于处理履约过程中的突发事件。
典型触发场景包括:仓库收货时发现货物质量不合格,需要暂停付款;供应商被曝出财务危机,需要暂停后续到货;市场价格剧烈波动,需要重新评估订单合理性。
冻结的设计原则:
- 冻结必须指定冻结类型和预计解冻时间,防止无限期冻结。
- 冻结期间,与订单相关的下游操作(收货、付款)全部暂停。
- 解冻时需要责任人确认,而不是自动恢复。确认解冻意味着风险已经消除,业务可以继续。
超时自动关闭
超时关闭是防止"僵尸订单"占用系统资源的机制。
不同状态的超时策略不同:
| 状态 | 超时时间 | 超时动作 |
|---|---|---|
| 草稿 | 7 天未提交 | 自动关闭 |
| 待审批 | 3 天未审批 | 提醒审批人,5 天后升级 |
| 已审批 | 30 天未收货 | 提醒采购员,60 天后冻结 |
超时关闭不是简单的删除,而是状态迁移到"已关闭",并记录关闭原因(超时),同时释放相关资源(如预算占用)。
状态回退
状态回退是状态机设计中最具争议的话题。严格来说,理想的状态机不允许回退——就像时间不能倒流。但在实际业务中,回退不可避免。
什么情况下允许回退
- 审批驳回:已提交的订单被审批人拒绝,需要从待审批回退到草稿。这是最常见的回退场景。
- 异常解冻:已冻结的订单经调查后确认无风险,需要从冻结回退到上一状态。
- 操作撤销:操作人员在短时间内(如 5 分钟内)发现自己操作错误,申请撤销。
回退的风险
允许回退会带来三个核心风险:
- 数据一致性破坏:如果"已审批"回退到"草稿",但供应商已经收到通知并开始备货,系统状态与实际情况就产生了不一致。
- 连锁反应复杂:一个订单状态回退,可能触发下游一系列动作的撤销。例如,已审批状态触发了库存预占,回退时需要释放预占;如果仓库已经开始拣货,回退会变得更加复杂。
- 审计追踪困难:频繁回退会让单据的历史轨迹变得混乱,审计时难以判断哪些操作是合理的、哪些是违规的。
回退的处理方式
我的建议是:用"正向流转"替代"物理回退"。
与其让状态真的退回去,不如定义一个新的正向状态来表达"回退"的语义。例如:
// 不推荐:物理回退
已审批 --撤销--> 草稿
// 推荐:正向流转到新状态
已审批 --撤销--> 已撤销(待修改)
已撤销 --修改完成--> 待审批
这样做的好处是:状态迁移始终是单向的,历史记录清晰,不会出现循环。而且"已撤销"作为一个独立状态,可以附加自己的校验规则,如撤销必须在 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 个用户会难以理解和记忆。异常状态尽量复用,例如"已取消"和"已关闭"在很多场景下可以合并。不要把"子状态"和"主状态"混为一谈,"待审批"是一个状态,而"待主管审批"和"待财务审批"是子流程,应该用审批流引擎来管理,而不是状态机。
命名规范
状态命名直接影响用户理解和开发效率。好的状态名有三个特征:
- 动宾结构或形容词短语:如"待审批""已审批""完全收货",一看就知道当前处于什么阶段。
- 无二义性:避免"处理中""审核中"这种模糊词汇。是"待审批"还是"审批中"?要明确区分"等待别人做"和"正在做"。
- 成对出现:有"待审批"就要有"已审批",有"部分收货"就要有"完全收货",形成清晰的对比关系。
可视化需求
状态机不能只存在于代码里,产品经理和业务员都需要看到它。
产品设计中应提供三种可视化:
- 单据状态条:在订单详情页顶部,用进度条形式展示当前状态和已完成的状态。这是用户最直观的感知方式。
- 状态迁移图:在系统管理后台,用图形化方式展示所有状态及其关系。当业务规则调整时,这张图就是开发和测试的参照。
- 操作按钮动态显示:当前状态下允许的操作才显示按钮,不允许的操作隐藏或置灰。这比让用户点完按钮再提示"当前状态不允许"要友好得多。
总结
状态机是 ERP 系统的"交通规则"。没有它,单据就像没有信号灯的车流,各行其是,最终必然撞车。
今天我们围绕采购订单,完整拆解了状态机的六个方面:状态定义、迁移规则、异常处理、回退策略、命名规范和可视化设计。这些原则不仅适用于采购订单,也适用于销售订单、入库单、对账单等任何有生命周期的业务单据。
记住两个核心判断:
第一,状态机的本质是约束。约束带来秩序,秩序带来可预测性。不要把状态机设计得过于灵活,灵活到失去约束的意义。
第二,状态变更的副作用比状态本身更重要。一个状态从 A 变成 B,真正的业务价值往往在于 B 触发的那一连串下游动作。设计状态机时,要把一半精力放在定义状态,一半精力放在定义副作用。
下一篇,我们将进入 ERP 产品设计的另一个核心话题——权限设计。当状态机规定了单据能怎么流转,权限体系则规定了谁能推动这个流转。状态机与权限,是一对不可分割的设计搭档。
往期回顾
- 业财通识26:功能模块划分——如何把业务流程变成系统菜单
- 业财通识25:业财对账——打通业务与财务的最后一公里
- 业财通识24:银行对账——银行流水与账本的逐笔核对
- 业财通识23:应付对账——管住每一笔该付的钱
- 业财通识22:应收对账——确保每一笔钱都收回来
- 业财通识21:库存成本计价——FIFO、加权平均与标准成本
- 业财通识20:安全库存——科学补货的数学基础
- 业财通识19:可用库存——为什么账上有货却不能卖
- 业财通识18:调拨——多仓协同的物流调度
- 业财通识17:盘点——账实一致的最后防线
- 业财通识16:出库——从销售发货到领料消耗
- 业财通识15:入库——四种场景下的库存增加
- 业财通识14:应收账款——从开票到回款的风险管控
- 业财通识13:价格策略——多维定价与动态调整
- 业财通识12:一个客户,为什么会有三条记录?——CRM 作为主数据底座的三层模型
- 业财通识11:从开票到收款,企业如何收回每一分钱?
- 业财通识10:当货物发出,系统里发生了什么?
- 业财通识09:订单确认前,系统如何防止坏账风险?
- 业财通识08:企业赚钱的第一步,从"潜在客户"到"销售合同"
- 业财通识07:业财难点之"暂估入账"与冲销
- 业财通识06:什么是采购在途?它对库存预测的价值
- 业财通识05:商品世界的基石——SPU 与 SKU
- 业财通识04:万事俱备,如何优雅地"打款"给供应商?
- 业财通识03:收到供应商账单,能直接付款吗?
- 业财通识02:当货物上门,系统里发生了什么?
- 业财通识01:企业花钱的第一步,从"购物清单"到"法律合同"
关于十三Tech
资深服务端研发工程师、架构师、AI 编程实践者。
专注分享真实的技术实践经验,持续记录企业系统、架构设计与 AI 编程实践。