2. 自动售货机状态机

任务目标

实现一个自动售货机的状态机,覆盖投币、选择商品、出货、找零主流程,并处理余额不足退款与无效商品退款两类异常。状态机采用经典 Mealy 型 FSM,事件驱动,无第三方依赖。

状态与事件定义

共 5 个状态:IDLE(待机)、ACCEPTING(接受投币)、DISPENSING(出货中)、CHANGING(找零中)、REFUNDING(退款中)。事件包括:COIN(投币)、SELECT(选择商品)、DISPENSE_DONE(出货完成)、CHANGE_DONE(找零完成)、REFUND_DONE(退款完成)。

关键完整代码

from enum import Enum, auto
from dataclasses import dataclass, field

class State(Enum):
    IDLE = auto()
    ACCEPTING = auto()
    DISPENSING = auto()
    CHANGING = auto()
    REFUNDING = auto()

class Event(Enum):
    COIN = auto()
    SELECT = auto()
    DISPENSE_DONE = auto()
    CHANGE_DONE = auto()
    REFUND_DONE = auto()

@dataclass
class Product:
    price: int
    stock: int = 5

@dataclass
class VendingMachine:
    products: dict = field(default_factory=lambda: {
        "A1": Product(10), "B2": Product(15), "C3": Product(20)
    })
    state: State = State.IDLE
    balance: int = 0
    selected: str | None = None
    refund_amount: int = 0

    def handle(self, event: Event, payload=None):
        if self.state == State.IDLE:
            if event == Event.COIN:
                self.balance += payload
                self.state = State.ACCEPTING
                return f"投币 {payload} 元,余额 {self.balance}"
            return "请先投币"

        elif self.state == State.ACCEPTING:
            if event == Event.COIN:
                self.balance += payload
                return f"投币 {payload} 元,余额 {self.balance}"
            if event == Event.SELECT:
                code = payload
                if code not in self.products:
                    self.refund_amount = self.balance
                    self.balance = 0
                    self.state = State.REFUNDING
                    return f"无效商品 {code},退款 {self.refund_amount} 元"
                prod = self.products[code]
                if prod.stock <= 0:
                    self.refund_amount = self.balance
                    self.balance = 0
                    self.state = State.REFUNDING
                    return f"商品 {code} 缺货,退款 {self.refund_amount} 元"
                if self.balance < prod.price:
                    self.refund_amount = self.balance
                    self.balance = 0
                    self.state = State.REFUNDING
                    return f"余额不足,退款 {self.refund_amount} 元"
                self.selected = code
                prod.stock -= 1
                self.balance -= prod.price
                self.state = State.DISPENSING
                return f"出货 {code},扣除 {prod.price} 元"
            return "请投币或选择商品"

        elif self.state == State.DISPENSING:
            if event == Event.DISPENSE_DONE:
                if self.balance > 0:
                    self.state = State.CHANGING
                    return f"出货完成,找零 {self.balance} 元"
                self.state = State.IDLE
                return "出货完成,无找零"

        elif self.state == State.CHANGING:
            if event == Event.CHANGE_DONE:
                self.balance = 0
                self.selected = None
                self.state = State.IDLE
                return "找零完成,回到待机"

        elif self.state == State.REFUNDING:
            if event == Event.REFUND_DONE:
                self.balance = 0
                self.selected = None
                self.state = State.IDLE
                return f"退款完成,回到待机"

        return f"当前状态 {self.state.name} 不能处理 {event.name}"

# 最小验证闭环
if __name__ == "__main__":
    vm = VendingMachine()
    print(vm.handle(Event.COIN, 10))       # 投币10
    print(vm.handle(Event.COIN, 5))        # 再投5,余额15
    print(vm.handle(Event.SELECT, "B2"))   # 选B2(15元),出货
    print(vm.handle(Event.DISPENSE_DONE))  # 出货完成,无找零
    print("--- 异常场景:余额不足 ---")
    vm2 = VendingMachine()
    print(vm2.handle(Event.COIN, 5))       # 投5
    print(vm2.handle(Event.SELECT, "C3"))  # C3需20,余额不足→退款
    print(vm2.handle(Event.REFUND_DONE))   # 退款完成

运行与验证

直接运行 python vending_machine.py,观察输出:

  • 正常流程:投币 10 → 投币 5 → 选 B2 → 出货完成,无找零,状态回到 IDLE。
  • 异常流程:投币 5 → 选 C3(需 20)→ 触发退款 5 元 → 退款完成回到 IDLE。

通过条件:所有 print 输出与代码逻辑一致,且状态最终均回到 IDLE,余额清零。可自行追加测试:投币 25 选 C3 后应找零 5 元;选择不存在的 X9 应立即退款。

状态机说明

该实现为同步事件驱动,每个 handle 调用处理一个事件并迁移状态。实际硬件场景中,DISPENSE_DONE、CHANGE_DONE、REFUND_DONE 通常由传感器或执行机构的中断/回调触发,本代码以显式事件模拟该异步边界,便于单测验证。

IDLE ACCEPTING DISPENSING CHANGING REFUNDING COIN(pay) SELECT(code) DISPENSE_DONE CHANGE_DONE / 余额=0 REFUND_DONE SELECT(无效/缺货/余额不足) COIN DISPENSE_DONE(余额=0)