从零到一:手把手教你构建自动售货机脚本的核心逻辑(附伪代码与避坑指南)
目录导读
- 自动售货机脚本的“灵魂”:状态机思维
- 核心模块拆解:库存、货币、交易与找零
- 关键算法:如何优雅地处理“并发购买”与“异常取消”?
- 实战伪代码:一段可直接改写的Python骨架
- 高频问答:新手最容易踩的5个逻辑陷阱
自动售货机看似简单,但它的逻辑本质是一个有限状态机(Finite State Machine),如果你直接写“if-else连环套”,脚本很快就会变成一锅粥,本文将结合主流嵌入式与后端设计模式,带你梳理一套可扩展、可测试的脚本方案。

状态机:先画流程图,再写代码
不要急着写if判断,先把机器状态枚举出来:待机(IDLE) → 接收货币(COIN_INSERTED) → 选择商品(SELECTING) → 出货(DISPENSING) → 找零(CHANGE_RETURN),每个状态只负责“当前动作”和“触发下一个状态的条件”,比如在COIN_INSERTED状态,唯一事件是“插入更多钱”或“选择商品”,其他输入(如反复按退货键)会被标记为无效请求,这种模式让你后期增加“扫码支付”时,只需新增一个QR_PAYING状态,不动核心代码。
库存与货币:用“字典”而不是“列表”
库存建议用{商品ID: {'价格': int, '数量': int}}结构,价格用最小货币单位(如分/美分)存储,避免浮点数精度问题(例如0.1+0.2≠0.3),货币模块要区分“已投入金额”和“临时暂存金额”——后者在确认出货前才扣减,找零算法采用贪心策略:优先使用大面额硬币(如25分→10分→5分→1分),但必须检查库存余额,否则会造成“找零不足”的尴尬。
并发与取消:引入“超时锁”与“事务回滚”
如果是物联网设备,用户可能在投币后突然拔电,脚本需要每10秒记录一次“当前状态快照”到非易失性存储,当检测到异常重启时,自动回滚到最近一个一致状态,对于“出货失败”(比如卡货),应当触发“退款指令”,且该指令优先于普通交易——这需要一个优先级队列,而不是简单的单线程顺序执行。
实战伪代码(Python风格)
class VendingMachine:
def __init__(self, stock):
self.state = "IDLE"
self.balance = 0 # 单位: 分
self.stock = stock # {id: {'price': 250, 'qty': 5}}
def insert_coin(self, value):
if self.state == "IDLE":
self.balance += value
self.state = "COIN_INSERTED"
elif self.state == "COIN_INSERTED":
self.balance += value
# 其他状态忽略
def select_item(self, item_id):
if self.state != "COIN_INSERTED":
return "请先投币"
item = self.stock.get(item_id)
if not item or item['qty'] <= 0:
return "缺货"
if self.balance < item['price']:
return "余额不足"
# 执行出货
item['qty'] -= 1
change = self.balance - item['price']
self.balance = 0
self.state = "IDLE"
return f"出货成功,找零{change}分"
注意:真实环境需加入try/except包裹出货硬件调用,且状态转移前置校验必须放在函数第一行。
高频问答:逻辑排雷指南
-
问:为什么我的找零逻辑容易“死循环”?
答:贪心算法会尝试用max(coin_stock)找零,但若某面额硬币库存为零,必须跳过该面额,建议循环内增加while remaining >= coin_value and coin_available双重判断。 -
问:如何处理用户投币后5分钟不选商品?
答:设计一个time.time()戳在每次状态变更时重置,后台守护线程检测到超时则强制退款并回到IDLE,注意退款函数要独立于交易主流程,避免被阻塞。 -
问:是否需要用数据库存交易记录?
答:嵌入式设备用SQLite,云端可用MySQL,但脚本核心逻辑不要直接操作数据库,而是通过事件钩子(如on_dispense(id))异步记录,防止IO卡顿。 -
问:脚本如何测试?
答:将“出货”、“退币”等物理动作抽象为接口,测试时替换为mock_actuator(),并用pytest跑状态转移矩阵,重点测试:缺货退款、余额不足、零钱不足、重复投币取消。 -
问:扫码支付怎么融合?
答:把“支付成功”当作一个外部回调事件,在主循环中,通过消息队列接收该事件,并强制将状态从WAIT_SCAN切换到SELECTING,而不是轮询网络状态。
自动售货机脚本的精髓在于“约束状态转移”和“硬件解耦”,不要试图在一个函数里干完所有事——把“决策”和“执行”分开,你的代码会像售货机内部的传送带一样,清晰而可靠,当遇到复杂需求时,先回到状态机图里画一笔,再让代码落地。