导读:餐饮、零售门店的小票和后厨联单一旦漏打,就是漏上菜、漏发货、对不上账;网络抖动后重试又常常打出两三张重复单。问题往往不在打印机本身,而在打印任务"发出去就当成功"。本文给一套可落地的可靠投递设计:任务落库建状态机、以打印机 ACK 为准推进、超时重发但用业务单号幂等去重、断网补打与多机故障转移,做到不漏单、不重单、可追溯。
常见第一版是:下单后后端直接调云打印厂商的开放接口,接口返回成功就完事。这条链路里有三段都会出问题:
只看 HTTP 200 无法覆盖后两段。可靠做法是把"打印"当成一条有生命周期的任务记录,以打印机真正出纸的回执(ACK)作为成功判据,而不是以接口受理为判据。
每个需要出纸的动作(结账单、后厨联单、退款单)生成一条打印任务,状态只能单向推进:
待发送 pending → 已发送 sent → 已确认 printed(收到出纸 ACK) 任意未确认状态下失败 → 失败重试 retrying(回到发送队列,重试次数 +1) 超过最大重试仍失败 → 需人工 failed(触发告警/转人工补打) 人工确认已出纸或手动补打成功 → 已闭环 closed
关键是 sent 不是终态:发出去没拿到 ACK,就仍然算"未完成",要继续追问。用一张任务表把状态、重试次数、业务单号都钉下来。
CREATE TABLE print_task (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_no VARCHAR(40) NOT NULL, -- 业务单号,如订单号+联单类型
printer_id VARCHAR(40) NOT NULL,
content_hash CHAR(32) NOT NULL, -- 打印内容指纹
status VARCHAR(12) NOT NULL DEFAULT 'pending',
retry_count INT NOT NULL DEFAULT 0,
max_retry INT NOT NULL DEFAULT 5,
last_ack_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_biz_printer (biz_no, printer_id), -- 同一业务单同一台机只一条任务
KEY idx_status_retry (status, retry_count)
);uk_biz_printer 是幂等的第一道闸:同一订单的后厨联单对同一台打印机,无论下单接口被重试多少次,任务只有一条。
重发不能简单地再调一次接口。要做两件事:发送带幂等键、回执做幂等确认。
import time
def dispatch_due_tasks(now, print_gateway):
tasks = query("SELECT * FROM print_task "
"WHERE status IN ('pending','retrying') "
"AND updated_at < %s LIMIT 200", now - 30)
for t in tasks:
if t["retry_count"] >= t["max_retry"]:
update_status(t["id"], "failed")
alert_ops(t) # 转人工,不再盲目重发
continue
try:
# 以业务单号作为厂商侧幂等键,平台对同一键只出一张
resp = print_gateway.send(
idempotency_key=t["biz_no"] + "#" + t["printer_id"],
printer_id=t["printer_id"],
content=render_content(t))
mark_sent(t["id"], resp.platform_sn)
except TimeoutError:
increase_retry(t["id"]) # 不判死,下一轮带同一幂等键再问
backoff(t["retry_count"]) # 指数退避,避免故障时狂打退避节奏建议 30 秒、1 分钟、2 分钟、5 分钟逐级拉长。即使平台第一次其实已收到、ACK 丢在回程,第二次请求带着相同幂等键,平台侧也只认一单,不会出第二张纸。
要区分两个回执:平台的"已受理"和打印机的"已打印"。前者表示任务进了云队列,后者才表示纸出来了。通过厂商的状态回调或主动查询,把打印机回传的状态码映射回任务状态。
def on_printer_callback(payload, sign_ok):
if not sign_ok:
return 401
biz_no, state = payload["idempotency_key"], payload["print_state"]
task = get_task_by_biz(biz_no)
if not task:
return 404
# 幂等确认:已是终态就直接返回,不重复推进、不重复告警
if task["status"] in ("printed", "closed"):
return 200
if state == "PRINTED":
update_status(task["id"], "printed", ack_at=payload["time"])
elif state in ("NO_PAPER", "OFFLINE", "COVER_OPEN"):
increase_retry(task["id"], reason=state) # 缺纸/掉线,等待恢复重发
return 200只有拿到 PRINTED 才置为已确认;缺纸、掉线、纸仓盖未合这类可恢复错误进入重试并带上原因,门店端可据此提示店员处理。
printed 的任务重新投递,两边都靠业务单号幂等,不会因为双通道而重单。biz_no、不同 printer_id,并在主任务上标记"已转移",避免主机恢复后又出一张。打印可靠性的本质是"任务化 + 回执驱动 + 幂等",与具体云打印厂商解耦:在业务侧维护统一的打印任务表和状态机,厂商能力收敛在一个网关适配器里(发送、查询、回调验签),换硬件或多厂商并存时业务代码不动。门店弱网环境建议把"平台回调 + 定时扫描主动查询"两条确认通道都打开,ACK 以先到且幂等者为准。
A:把每次出纸建成带状态机的打印任务,以打印机出纸回执为成功判据,未确认任务由定时扫描和平台回调双通道兜底重投,超过重试上限转人工,即可覆盖断网、缺纸、超时各类漏单。
A:发送时以"业务单号+打印机号"作为幂等键,任务表对该组合加唯一约束,平台对同一幂等键只出一张;回执确认也做幂等,已是终态的任务不再重复推进。
A:会,打印机重新在线会拉取未确认任务,后端定时扫描也会重投超时未确认的任务,两条通道都按业务单号幂等;若主打印机长时间不可用,可转移到备用机并在原任务标记,避免主机恢复后重复出纸。
以上为门店云打印可靠投递的一套状态机实践,状态码、退避节奏与故障转移策略请结合所用打印厂商的实时接口文档调整。抓住"发出去不等于打出来、重试必须幂等"两点,漏单和重单这两个最常见的门店纠纷基本可以消除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。