From bab09f4eb3a7535a34b377f5e66b21737b417702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A6=E9=BE=99=E8=A8=80?= Date: Fri, 24 Jul 2026 19:37:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=89=98=E7=9B=98=E7=8A=B6=E6=80=81=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_business/domain/enums.py | 30 ++++++++ .../domain/pallet_state_machine.py | 38 ++++++++++ .../module_business/test_state_machine.py | 76 +++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 shierfeng-fastapi-backend/module_business/domain/enums.py create mode 100644 shierfeng-fastapi-backend/module_business/domain/pallet_state_machine.py create mode 100644 shierfeng-fastapi-backend/tests/module_business/test_state_machine.py diff --git a/shierfeng-fastapi-backend/module_business/domain/enums.py b/shierfeng-fastapi-backend/module_business/domain/enums.py new file mode 100644 index 0000000..c061d8c --- /dev/null +++ b/shierfeng-fastapi-backend/module_business/domain/enums.py @@ -0,0 +1,30 @@ +from enum import Enum + + +class PalletFlowState(str, Enum): + UNSTOCKED = 'UNSTOCKED' + EMPTY_IN_WAREHOUSE = 'EMPTY_IN_WAREHOUSE' + FILLED_IN_WAREHOUSE = 'FILLED_IN_WAREHOUSE' + AT_SITE_UNUSED = 'AT_SITE_UNUSED' + AT_SITE_PARTIAL = 'AT_SITE_PARTIAL' + AT_SITE_EMPTY = 'AT_SITE_EMPTY' + RETURNING = 'RETURNING' + + +class DocumentStatus(str, Enum): + DRAFT = 'DRAFT' + CONFIRMED = 'CONFIRMED' + + +class ReturnStatus(str, Enum): + REQUESTED = 'REQUESTED' + CONFIRMED = 'CONFIRMED' + + +class PalletEventType(str, Enum): + INBOUND_CONFIRMED = 'INBOUND_CONFIRMED' + FILLING_CONFIRMED = 'FILLING_CONFIRMED' + SHIPMENT_CONFIRMED = 'SHIPMENT_CONFIRMED' + USAGE_RECORDED = 'USAGE_RECORDED' + RETURN_REQUESTED = 'RETURN_REQUESTED' + RETURN_CONFIRMED = 'RETURN_CONFIRMED' diff --git a/shierfeng-fastapi-backend/module_business/domain/pallet_state_machine.py b/shierfeng-fastapi-backend/module_business/domain/pallet_state_machine.py new file mode 100644 index 0000000..6b08a3f --- /dev/null +++ b/shierfeng-fastapi-backend/module_business/domain/pallet_state_machine.py @@ -0,0 +1,38 @@ +from module_business.domain.enums import PalletFlowState + + +class InvalidPalletTransition(ValueError): + """托盘状态迁移不符合业务规则。""" + + +class PalletStateMachine: + _ALLOWED_TRANSITIONS = frozenset( + { + (PalletFlowState.UNSTOCKED, PalletFlowState.EMPTY_IN_WAREHOUSE), + ( + PalletFlowState.EMPTY_IN_WAREHOUSE, + PalletFlowState.FILLED_IN_WAREHOUSE, + ), + (PalletFlowState.FILLED_IN_WAREHOUSE, PalletFlowState.AT_SITE_UNUSED), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.AT_SITE_PARTIAL), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.AT_SITE_EMPTY), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.RETURNING), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.AT_SITE_PARTIAL), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.AT_SITE_EMPTY), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.RETURNING), + (PalletFlowState.AT_SITE_EMPTY, PalletFlowState.RETURNING), + (PalletFlowState.RETURNING, PalletFlowState.FILLED_IN_WAREHOUSE), + (PalletFlowState.RETURNING, PalletFlowState.EMPTY_IN_WAREHOUSE), + } + ) + + @classmethod + def ensure_transition( + cls, + current: PalletFlowState, + target: PalletFlowState, + ) -> None: + if (current, target) not in cls._ALLOWED_TRANSITIONS: + raise InvalidPalletTransition( + f'不允许的托盘状态迁移:{current.value} -> {target.value}' + ) diff --git a/shierfeng-fastapi-backend/tests/module_business/test_state_machine.py b/shierfeng-fastapi-backend/tests/module_business/test_state_machine.py new file mode 100644 index 0000000..9fb125b --- /dev/null +++ b/shierfeng-fastapi-backend/tests/module_business/test_state_machine.py @@ -0,0 +1,76 @@ +import unittest + +from module_business.domain.enums import ( + DocumentStatus, + PalletEventType, + PalletFlowState, + ReturnStatus, +) +from module_business.domain.pallet_state_machine import ( + InvalidPalletTransition, + PalletStateMachine, +) + + +class PalletStateMachineTest(unittest.TestCase): + ALLOWED_TRANSITIONS = ( + (PalletFlowState.UNSTOCKED, PalletFlowState.EMPTY_IN_WAREHOUSE), + (PalletFlowState.EMPTY_IN_WAREHOUSE, PalletFlowState.FILLED_IN_WAREHOUSE), + (PalletFlowState.FILLED_IN_WAREHOUSE, PalletFlowState.AT_SITE_UNUSED), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.AT_SITE_PARTIAL), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.AT_SITE_EMPTY), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.RETURNING), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.AT_SITE_PARTIAL), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.AT_SITE_EMPTY), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.RETURNING), + (PalletFlowState.AT_SITE_EMPTY, PalletFlowState.RETURNING), + (PalletFlowState.RETURNING, PalletFlowState.FILLED_IN_WAREHOUSE), + (PalletFlowState.RETURNING, PalletFlowState.EMPTY_IN_WAREHOUSE), + ) + + def test_confirmed_flow_is_allowed(self) -> None: + for current, target in self.ALLOWED_TRANSITIONS: + PalletStateMachine.ensure_transition(current, target) + + def test_refill_at_customer_site_is_rejected(self) -> None: + with self.assertRaises(InvalidPalletTransition): + PalletStateMachine.ensure_transition( + PalletFlowState.AT_SITE_EMPTY, + PalletFlowState.FILLED_IN_WAREHOUSE, + ) + + def test_confirmed_flow_is_the_only_allowed_transition_set(self) -> None: + actual_allowed = set() + for current in PalletFlowState: + for target in PalletFlowState: + try: + PalletStateMachine.ensure_transition(current, target) + except InvalidPalletTransition: + continue + actual_allowed.add((current, target)) + + self.assertEqual(set(self.ALLOWED_TRANSITIONS), actual_allowed) + + +class PalletEnumTest(unittest.TestCase): + def test_supporting_enum_values_match_domain_contract(self) -> None: + self.assertEqual(['DRAFT', 'CONFIRMED'], [item.value for item in DocumentStatus]) + self.assertEqual( + ['REQUESTED', 'CONFIRMED'], + [item.value for item in ReturnStatus], + ) + self.assertEqual( + [ + 'INBOUND_CONFIRMED', + 'FILLING_CONFIRMED', + 'SHIPMENT_CONFIRMED', + 'USAGE_RECORDED', + 'RETURN_REQUESTED', + 'RETURN_CONFIRMED', + ], + [item.value for item in PalletEventType], + ) + + +if __name__ == '__main__': + unittest.main()