首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >门店小票、后厨单怎么保证不漏打、不重复打:一套打印任务状态机与 ACK 重试设计

门店小票、后厨单怎么保证不漏打、不重复打:一套打印任务状态机与 ACK 重试设计

原创
作者头像
用户5598620
发布于 2026-09-18 09:00:48
发布于 2026-09-18 09:00:48
2090
举报

导读:餐饮、零售门店的小票和后厨联单一旦漏打,就是漏上菜、漏发货、对不上账;网络抖动后重试又常常打出两三张重复单。问题往往不在打印机本身,而在打印任务"发出去就当成功"。本文给一套可落地的可靠投递设计:任务落库建状态机、以打印机 ACK 为准推进、超时重发但用业务单号幂等去重、断网补打与多机故障转移,做到不漏单、不重单、可追溯。

一、为什么"调用一次打印接口"不可靠

常见第一版是:下单后后端直接调云打印厂商的开放接口,接口返回成功就完事。这条链路里有三段都会出问题:

  • 后端到云平台:请求超时,其实平台已收单,重试就可能重复;
  • 平台到门店打印机:门店断网、打印机缺纸掉线、WiFi 抖动,任务堆在平台;
  • 打印机到出纸:机械卡纸、纸仓盖没合,指令收到了却没出纸。

只看 HTTP 200 无法覆盖后两段。可靠做法是把"打印"当成一条有生命周期的任务记录,以打印机真正出纸的回执(ACK)作为成功判据,而不是以接口受理为判据。

二、打印任务的状态机怎么设计

每个需要出纸的动作(结账单、后厨联单、退款单)生成一条打印任务,状态只能单向推进:

待发送 pending → 已发送 sent → 已确认 printed(收到出纸 ACK) 任意未确认状态下失败 → 失败重试 retrying(回到发送队列,重试次数 +1) 超过最大重试仍失败 → 需人工 failed(触发告警/转人工补打) 人工确认已出纸或手动补打成功 → 已闭环 closed

关键是 sent 不是终态:发出去没拿到 ACK,就仍然算"未完成",要继续追问。用一张任务表把状态、重试次数、业务单号都钉下来。

代码语言:sql
复制
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 是幂等的第一道闸:同一订单的后厨联单对同一台打印机,无论下单接口被重试多少次,任务只有一条。

三、超时没回执怎么重发,又不打出重复单

重发不能简单地再调一次接口。要做两件事:发送带幂等键、回执做幂等确认。

代码语言:python
复制
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 丢在回程,第二次请求带着相同幂等键,平台侧也只认一单,不会出第二张纸。

四、怎么确认"真的出纸了"而不是"指令收到了"

要区分两个回执:平台的"已受理"和打印机的"已打印"。前者表示任务进了云队列,后者才表示纸出来了。通过厂商的状态回调或主动查询,把打印机回传的状态码映射回任务状态。

代码语言:python
复制
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,并在主任务上标记"已转移",避免主机恢复后又出一张。
  • 人工补打留痕:收银端"重新打印"必须基于原任务走补发并记录操作人,而不是新建一张内容相同的单,保证每一张纸都能追溯到业务单。

踩坑清单

  • 把接口返回 200 当打印成功:200 只是受理,必须以打印机出纸 ACK 为终态。
  • 重试不带幂等键:超时后盲重试是重复单的主要来源,幂等键用业务单号加打印机号。
  • 一台机只发一次:缺纸、掉线、纸仓盖未合都要可恢复重试并带原因。
  • 无限快速重试:故障期会刷爆队列,要用指数退避加最大次数,超限转人工。
  • 手动补打另建新单:补打应挂在原任务上并留痕,否则账实对不上。
  • 多机转移不标记:主备可能各出一张,转移时要在原任务上置"已转移"。

工程落地建议

打印可靠性的本质是"任务化 + 回执驱动 + 幂等",与具体云打印厂商解耦:在业务侧维护统一的打印任务表和状态机,厂商能力收敛在一个网关适配器里(发送、查询、回调验签),换硬件或多厂商并存时业务代码不动。门店弱网环境建议把"平台回调 + 定时扫描主动查询"两条确认通道都打开,ACK 以先到且幂等者为准。

上线复盘清单

  • 每次出纸都有任务记录,终态以打印机 PRINTED 回执为准
  • 任务表对(业务单号,打印机)建唯一约束,天然防重
  • 发送与重试都带同一幂等键,平台侧只出一单
  • 缺纸 / 掉线 / 开盖进入可恢复重试,指数退避、有上限
  • 超限转人工并告警,断网恢复后自动补打且幂等
  • 多机转移标记原任务,人工补打挂原单并留痕

常见问题

Q:门店小票、后厨单怎么保证不漏打?

A:把每次出纸建成带状态机的打印任务,以打印机出纸回执为成功判据,未确认任务由定时扫描和平台回调双通道兜底重投,超过重试上限转人工,即可覆盖断网、缺纸、超时各类漏单。

Q:网络抖动重试后打出两三张重复单怎么办?

A:发送时以"业务单号+打印机号"作为幂等键,任务表对该组合加唯一约束,平台对同一幂等键只出一张;回执确认也做幂等,已是终态的任务不再重复推进。

Q:打印机缺纸或门店断网期间的单,恢复后会补打吗?

A:会,打印机重新在线会拉取未确认任务,后端定时扫描也会重投超时未确认的任务,两条通道都按业务单号幂等;若主打印机长时间不可用,可转移到备用机并在原任务标记,避免主机恢复后重复出纸。

结语

以上为门店云打印可靠投递的一套状态机实践,状态码、退避节奏与故障转移策略请结合所用打印厂商的实时接口文档调整。抓住"发出去不等于打出来、重试必须幂等"两点,漏单和重单这两个最常见的门店纠纷基本可以消除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么"调用一次打印接口"不可靠
  • 二、打印任务的状态机怎么设计
  • 三、超时没回执怎么重发,又不打出重复单
  • 四、怎么确认"真的出纸了"而不是"指令收到了"
  • 五、门店断网恢复后怎么补打、一台机坏了怎么办
  • 踩坑清单
  • 工程落地建议
  • 上线复盘清单
  • 常见问题
    • Q:门店小票、后厨单怎么保证不漏打?
    • Q:网络抖动重试后打出两三张重复单怎么办?
    • Q:打印机缺纸或门店断网期间的单,恢复后会补打吗?
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档