baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
"""通用状态机服务。
设计:
- 单据状态变化走 StateMachine.transition(),不在 model.save() 里散写
- 支持 guard(条件校验)、action(副作用:扣库存/生成应收)、hook(插件/AI Agent/Webhook)
- 可被 view / service / task 统一调用
- 默认包 transaction.atomic();use_transaction=False 用于纯内存测试
用法:
sm = StateMachine(
states=("draft", "confirmed", "posted", "closed", "cancelled"),
initial="draft",
transitions={
"draft": {"confirm": "confirmed", "cancel": "cancelled"},
"confirmed": {"post": "posted", "cancel": "cancelled"},
"posted": {"close": "closed"},
},
guards={
("draft", "confirmed", "confirm"): require_draft,
},
actions={
("draft", "confirmed", "confirm"): on_confirm,
},
hooks={"post": [notify_webhook, log_audit]},
)
sm.transition(instance, "confirm", user=request.user)
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterable, Optional
from django.db import transaction
@dataclass
class TransitionContext:
instance: object
from_state: str
to_state: str
event: str
user: Optional[object] = None
extra: dict = field(default_factory=dict)
class StateTransitionError(Exception):
"""状态机非法迁移。"""
pass
class StateMachine:
def __init__(
self,
states: Iterable[str],
initial: str,
transitions: dict,
guards: dict | None = None,
actions: dict | None = None,
hooks: dict | None = None,
) -> None:
self.states = tuple(states)
self.initial = initial
self.transitions = {s: dict(es) for s, es in transitions.items()}
self.guards = guards or {}
self.actions = actions or {}
self.hooks = hooks or {}
if initial not in self.states:
raise ValueError(f"initial state {initial!r} not in states")
# --- 公共 API ---
def get_next_states(self, current: str) -> list[str]:
return list(self.transitions.get(current, {}).values())
def get_events(self, current: str) -> list[str]:
return list(self.transitions.get(current, {}).keys())
def transition(
self,
instance,
event: str,
*,
user=None,
extra: dict | None = None,
use_transaction: bool = True,
) -> TransitionContext:
"""执行一次状态迁移。
1. guard 校验
2. 改 instance.state
3. action 执行
4. hook 触发
5. 返回 TransitionContext
use_transaction=False 时不包 transaction.atomic()(用于纯内存测试)。
"""
if use_transaction:
with transaction.atomic():
return self._run(instance, event, user=user, extra=extra)
return self._run(instance, event, user=user, extra=extra)
def _run(self, instance, event, *, user, extra):
extra = dict(extra or {})
from_state = getattr(instance, "state", None)
if from_state not in self.transitions:
raise StateTransitionError(
f"current state {from_state!r} has no outgoing transitions"
)
if event not in self.transitions[from_state]:
raise StateTransitionError(
f"event {event!r} not allowed from state {from_state!r}"
)
to_state = self.transitions[from_state][event]
ctx = TransitionContext(
instance=instance,
from_state=from_state,
to_state=to_state,
event=event,
user=user,
extra=extra,
)
guard = self.guards.get((from_state, to_state, event))
if guard:
try:
ok = guard(ctx)
except Exception:
raise
else:
if ok is False:
raise StateTransitionError(
f"guard rejected transition {from_state} -[ {event} ]-> {to_state}"
)
instance.state = to_state
# save 由调用方负责(Django model.save() 或测试 stub)
action = self.actions.get((from_state, to_state, event))
if action:
action(ctx)
for hook in self.hooks.get(event, []):
hook(ctx)
return ctx
def can_transition(self, instance, event: str) -> bool:
from_state = getattr(instance, "state", None)
return event in self.transitions.get(from_state, {})