功能说明
订单、工单和审批流都有一组有限状态,以及“什么事件能把状态推进到哪一步”的规则。这段代码用 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);
}