← 中文资源

简体中文版本

用枚举约束订单状态流转

把订单状态建模为枚举,只允许下单、支付、发货和取消等合法流转。

阅读英文原文

功能说明

订单、工单和审批流都有一组有限状态,以及“什么事件能把状态推进到哪一步”的规则。这段代码用 Rust 枚举表示状态,用 match 集中定义合法流转;任何未列出的组合都会返回带来源状态和事件名的错误。

把规则集中在一个函数里,可以避免业务代码到处写 if state == ...,也便于测试和审计。示例中的规则是:草稿可下单或取消,已下单可支付或取消,已支付只能发货,已发货不能再变更。

关键代码

OrderState 派生 Copy,因此可以在 match 中按值比较而不移动。TransitionError::InvalidTransition 携带 from 和 event,方便日志和前端提示。

Order::apply 用 match (self.state, event) 一次性枚举所有合法组合,Draft | Placed 共享取消分支;兜底分支直接返回错误,不修改状态。只有匹配成功时才执行 self.state = next,因此失败不会产生半更新。

使用注意

事件目前是 &'static str,拼写错误只能在运行时发现;生产代码通常把事件也建模为枚举,并让编译器检查穷尽性。状态持久化时不要直接存枚举序号,应存稳定的字符串或显式编码,避免枚举顺序变化导致历史数据错位。

这里的取消规则只允许草稿和已下单,已支付订单不能取消;真实业务可能还需要退款状态。并发更新要用数据库乐观锁或事务,单进程内的状态机无法防止两个请求同时读到旧状态。

验证方式

示例断言从草稿直接支付会失败、下单和支付会成功、已支付取消会失败、发货成功、重复发货失败,最终状态为 Shipped。可以加入 Cancelled 后任意事件都失败的用例,或把事件改成枚举后让编译器帮助发现遗漏分支。

代码示例

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OrderState {
    Draft,
    Placed,
    Paid,
    Shipped,
    Cancelled,
}

#[derive(Debug, PartialEq, Eq)]
enum TransitionError {
    InvalidTransition {
        from: OrderState,
        event: &'static str,
    },
}

struct Order {
    state: OrderState,
}

impl Order {
    fn new() -> Self {
        Self {
            state: OrderState::Draft,
        }
    }

    fn apply(&mut self, event: &'static str) -> Result<OrderState, TransitionError> {
        use OrderState::*;
        let next = match (self.state, event) {
            (Draft, "place") => Placed,
            (Placed, "pay") => Paid,
            (Paid, "ship") => Shipped,
            (Draft | Placed, "cancel") => Cancelled,
            (_, _) => {
                return Err(TransitionError::InvalidTransition {
                    from: self.state,
                    event,
                });
            }
        };
        self.state = next;
        Ok(next)
    }
}

fn main() {
    let mut order = Order::new();
    assert_eq!(
        order.apply("pay"),
        Err(TransitionError::InvalidTransition {
            from: OrderState::Draft,
            event: "pay",
        })
    );
    assert_eq!(order.apply("place"), Ok(OrderState::Placed));
    assert_eq!(order.apply("pay"), Ok(OrderState::Paid));
    assert_eq!(
        order.apply("cancel"),
        Err(TransitionError::InvalidTransition {
            from: OrderState::Paid,
            event: "cancel",
        })
    );
    assert_eq!(order.apply("ship"), Ok(OrderState::Shipped));
    assert_eq!(
        order.apply("ship"),
        Err(TransitionError::InvalidTransition {
            from: OrderState::Shipped,
            event: "ship",
        })
    );
    println!("final state: {:?}", order.state);
}