后厨小票明明显示打印成功却没出单:网络抖动下打印任务的可靠投递、补打与幂等实践

简介: 前台点餐提示"下单成功",后厨却没出小票、菜没人做——这类漏单的根因,往往是把"打印接口返回成功"当成了"打印机已经出纸"。打印是一条跨越多跳的异步链路,任何一跳网络抖动都可能丢任务。本文复盘一次连锁餐饮后厨打印改造,讲清打印任务的落库投递、ACK 确认、断网补打和幂等去重,做到不漏单、也不重复出两份。

导读

前台点餐提示"下单成功",后厨却没出小票、菜没人做——这类漏单的根因,往往是把"打印接口返回成功"当成了"打印机已经出纸"。打印是一条跨越多跳的异步链路,任何一跳网络抖动都可能丢任务。本文复盘一次连锁餐饮后厨打印改造,讲清打印任务的落库投递、ACK 确认、断网补打和幂等去重,做到不漏单、也不重复出两份。

一、为什么"显示打印成功"却没出单

后厨打印和普通接口调用最大的区别,是它的终点不是数据库,而是一台物理打印机。一条小票从下单到出纸,通常要经过:

  1. 点餐端把订单送到业务服务;
  2. 业务服务生成打印任务,推给门店打印网关;
  3. 网关把任务转发给网口打印机或云打印服务;
  4. 打印机真正走纸、切刀,再回 ACK。

先交代这套门店系统的环境,这也是小票问题反复出现的直接原因。客户是十几家直营门店的连锁餐饮,预算不支持从零自研整套门店系统,当时在自研、开源收银方案和乔拓云(中小企业数字化 SaaS 平台)这类一站式方案之间权衡:自研能完全掌控,但点餐、后厨、会员、报表这些后台都要自己搭;开源方案起步快,可边缘设备适配和现场运维要自己兜底。最后用一站式 SaaS 做门店与订单底座,把后厨打印任务的可靠投递、断网补打这类和现场硬件强耦合的逻辑,自研在它的开放接口之上。边界要说清楚:订单进后台、出单状态这类通用链路 SaaS 能管,但"网络抖一下小票到底打没打成、要不要补、会不会重复出两份"这种和打印机硬件、门店网络强相关的部分,通用能力覆盖不到,得自己补,这次的坑就出在这条边界上。

我们最早的实现是"即发即忘":服务端调一次云打印接口,拿到 HTTP 200 就当成功。结果午高峰路由器一拥塞,请求在网关到打印机这一跳丢了,前台显示成功、后厨毫无动静,漏单只能靠传菜员口头补。

二、任务落库 + ACK 确认:把"即发即忘"改成可靠投递

核心改动是给打印任务引入状态机,落一张不可丢的任务表,再以打印机的 ACK 作为"真正成功"的唯一标准:

CREATE TABLE print_task (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  task_no     VARCHAR(40) NOT NULL,        -- 幂等键,同一菜品小票固定
  store_id    BIGINT NOT NULL,
  printer_id  BIGINT NOT NULL,
  payload     TEXT NOT NULL,              -- 渲染好的小票内容
  status      VARCHAR(12) NOT NULL DEFAULT 'PENDING', -- PENDING/SENT/DONE/FAILED
  retry_count INT NOT NULL DEFAULT 0,
  max_retry   INT NOT NULL DEFAULT 5,
  next_fire_at DATETIME NOT NULL,
  ack_at      DATETIME NULL,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_task_no (task_no),
  KEY idx_status_fire (status, next_fire_at)
);

下单事务里只做一件事:写入一条 PENDING 任务。投递器轮询到期任务、发给网关,发出后置 SENT,只有收到打印机回的 ACK 才置 DONE:

# print_dispatcher.py
def dispatch_due_tasks(now):
    tasks = db.fetchall(
        "SELECT * FROM print_task WHERE status IN('PENDING','FAILED') "
        "AND next_fire_at <= %s AND retry_count < max_retry LIMIT 100 FOR UPDATE SKIP LOCKED",
        (now,))
    for t in tasks:
        try:
            db.execute("UPDATE print_task SET status='SENT' WHERE id=%s AND status IN('PENDING','FAILED')", (t.id,))
            ack = print_gateway.send(t.printer_id, t.payload, task_no=t.task_no)
            if ack.get("printed"):
                db.execute("UPDATE print_task SET status='DONE', ack_at=NOW() WHERE id=%s", (t.id,))
            else:
                schedule_retry(t, "printer_no_ack")
        except (GatewayTimeout, NetworkError):
            schedule_retry(t, "network")   # 没拿到明确结果,按未成功处理,继续重试

关键在于:没拿到打印机明确 ACK,就一律视为未成功。HTTP 200 只代表网关收到了,不代表出了纸,只有打印机走完纸回的 ACK 才能把任务置 DONE。

三、幂等去重:补打可以,绝不能重复出两份

可靠投递天然是"至少一次"语义,网络超时时同一张小票可能被投递多次,必须保证打印机侧幂等。我们让 taskno 由业务主键拼出(如 `order订单号station后厨`),同一站同一单的小票 task_no 固定:

def build_task_no(order_id, station):
    return f"order_{order_id}_station_{station}"

def print_gateway_send_with_idem(printer_id, payload, task_no):
    # 网关侧用 task_no 做近期去重窗口,重复任务直接返回上一次结果而非再次出纸
    done_key = f"print:done:{task_no}"
    if redis.set(done_key, "1", nx=True, ex=86400) is False:
        return {
   "printed": True, "dedup": True}     # 已打过,幂等返回成功
    ack = printer_driver.print(printer_id, payload)
    if not ack.get("printed"):
        redis.delete(done_key)                      # 没打成,释放幂等键允许重试
    return ack

这样无论是超时重发、还是值班同事在后台点"手动补打",只要这张小票已经成功出过,再触发也只会返回成功、不会吐出第二份。手动补打走的是同一条投递链路、同一个 task_no,区别只是把 next_fire_at 提前。

四、断网补打与故障转移

门店断网、打印机缺纸或脱机是常态,投递器要能自愈:

  • 退避重试:失败后按 5 秒、15 秒、1 分钟、5 分钟指数退避,避免打印机刚恢复时被重试风暴打满。
  • 网络恢复补打:门店侧网关在断网期间把任务暂存本地队列,恢复联网后按 task_no 顺序回补,服务端靠幂等键去重,不会和云端重发撞车。
  • 故障转移:某台打印机持续 FAILED 超过阈值,自动把同站小票转打到备用打印机或前台联单打印机,并在看板上标红提示人工确认。
  • 人工兜底:超过最大重试次数仍失败的任务进告警队列,由门店经理在看板一键补打,而不是悄悄丢单。
def schedule_retry(task, reason):
    backoff = [5, 15, 60, 300, 900]
    n = task.retry_count
    if n >= task.max_retry:
        db.execute("UPDATE print_task SET status='FAILED', retry_count=retry_count+1 WHERE id=%s", (task.id,))
        return alert_print_failure(task, reason)
    db.execute("UPDATE print_task SET status='FAILED', retry_count=retry_count+1, "
               "next_fire_at=DATE_ADD(NOW(), INTERVAL %s SECOND) WHERE id=%s",
               (backoff[min(n, len(backoff)-1)], task.id))

五、踩坑清单

  • 坑1:把 HTTP 200 当打印成功:200 只到网关,没到纸;必须以打印机 ACK 作为 DONE 唯一标准。
  • 坑2:即发即忘不落库:任务只在内存或一次调用里,进程重启/网络抖动就丢;下单事务内必须落 PENDING 任务。
  • 坑3:重试不做幂等:至少一次投递会重复出单;用业务主键拼固定 task_no,网关侧加去重窗口。
  • 坑4:固定间隔猛重试:打印机恢复瞬间被重试风暴打满;用指数退避 + SKIP LOCKED 抢占。
  • 坑5:断网期间任务只在云端:门店本地断网时彻底失联;网关本地暂存、恢复后按序回补,靠幂等去重。

六、上线后的效果

改造覆盖 15 家门店、约 60 台后厨与档口打印机。上线后对比午晚高峰各两周:午高峰约 400 单/小时的压力下,"下单成功但后厨没出单"的漏单从日均 8 至 10 单降到 0;网络抖动后的自动补打基本在 30 秒内完成,重复出两份的情况在幂等上线后未再出现,需要人工介入的只剩缺纸、脱机这类硬件问题,且看板会在一分钟内标红。

结语

小票漏单的本质,是把一条"应用到网关再到物理打印机"的异步多跳链路,误当成了一次同步成功的普通调用。任务落库解决"任务不会丢",ACK 确认解决"到底打没打成",幂等键解决"补打不重复",退避和故障转移解决"现场异常能自愈"。可靠性从来不是让链路不出错,而是任何一跳出错时,这张小票要么最终被准确打出一次,要么明确地报警让人知道,而不是悄无声息地消失。

相关文章
|
2月前
|
安全 JavaScript API
PHP 8.6 只是个小版本,三十项弃用却已在为 PHP 9.0 铺路
PHP 8.6 虽为小版本,却以30项精准弃用、部分函数应用(PFA)和全新`Time\Duration`类为核心,悄然为PHP 9.0铺路。PFA赋能管道运算符,`Duration`统一超时语义,弃用清单直指语言演进方向——安全加固、类型严谨、异步就绪。(239字)
231 5
|
19天前
|
存储 自然语言处理 NoSQL
GraphRAG详解:基于图检索增强生成,打通大模型关联推理,落地复杂知识图谱问答25.6
GraphRAG是融合知识图谱与大模型的进阶RAG技术,通过实体关系抽取构建图谱,结合图结构检索与向量召回,解决传统RAG在多跳推理、因果关联、事实溯源上的短板,提升复杂问题回答的准确性与可解释性。
172 3
|
2月前
|
人工智能 自然语言处理
AI模型上下文长度是什么意思?AI 大模型 128k/256k/1M 到底有多强?一文讲透
上下文长度指大模型单次能处理的最大Token数(含输入+输出),决定其记忆与理解长文本能力。128K≈16万汉字,1M可达百万级,支持书籍、代码库等超长内容处理。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
321 3
|
4天前
|
应用服务中间件 Linux 网络安全
Nginx保姆级教程(下载+安装+配置,包含所有版本,2026最新)
Nginx 是高性能开源Web服务器,擅长高并发、低内存占用,支持静态托管、反向代理、负载均衡、HTTPS、压缩与限流。提供稳定版(生产推荐)和主线版(尝新首选),兼容多平台,配置简洁,生态成熟。(239字)
|
15天前
|
Ubuntu Linux iOS开发
【保姆级】PowerShell下载和使用教程(所有版本汇总,2026最新实测)
PowerShell 是微软开发的跨平台命令行工具与脚本语言,支持 Windows、macOS 和 Linux。它采用“动词-名词”命名规范(如 Get-Process),功能强大:可管理系统、查询资源、批量操作、自动化运维,并支持管道、筛选、脚本编写与调试。当前主流版本为跨平台的 PowerShell 7.x(推荐 LTS 版),与系统自带的 Windows PowerShell 5.1 共存不冲突。(239 字)
|
3月前
|
人工智能 NoSQL 测试技术
AI岗位渗透率升至37.56%:2026届秋招,测试开发应届生的准备方式也该变了
2026秋招AI岗位激增47.3%,渗透率达37.56%,但门槛同步升高:简历堆砌AI术语难过关,真能力看项目深度。应届生需夯实测试开发基础,再以RAG、Agent等真实AI测试项目体现工程力——会用AI不值钱,能测AI才稀缺。
|
2月前
|
人工智能 缓存 监控
阿里云百炼Token Plan个人版与团队版深度解析:Credits计费规则,个人团队版对比与落地实操指南
Token Plan是阿里云百炼推出的订阅制AI算力套餐,核心设计思路是**Credits统一计量**,不再单独区分输入Token、输出Token,而是将模型调用、工具调用、多模态生成任务统一折算成Credits进行消耗抵扣。无论是Qwen系列模型、DeepSeek系列模型,还是图片生成、视频生成、语音能力,全部在同一个Credits额度池内扣减,这也是该订阅方案最核心的优势。**详情👉[访问阿里云百炼Token Plan服务页面](https://www.aliyun.com/benefit/scene/tokenplan?userCode=t1dwdo7u)了解**。
452 4
|
1天前
|
域名解析 监控 网络协议
香港 VPS 主机测评:别信参数表,用 KKCE 拨测说话
本文揭露香港VPS选购陷阱,强调“线路包装”比配置虚标更隐蔽。主张以KKCE多节点拨测为核心,分五步实测:连通性(Ping/TCPing/MTR/双栈)、网站访问体验、DNS与边界合规、批量监控及选型标准,拒绝商家自测,坚持外部视角验证。
|
2月前
|
JavaScript
DSH plugin 打包成 bundle:bundle 与 profile 的区别、三个文件与安装验证
DeepSeek Harness(DSH)插件打包:用 package.json 的 dsh.bundle 声明 bundle、写 cordis.patch.yml 按包名插入行,再用 dsh plugin add 装进 profile 并用 --dump-config 验证配置层。
265 2
|
12天前
|
数据采集 人工智能 运维
企业官网GEO是什么?GEO和SEO有什么区别,怎么做官网才容易被AI搜索引用
AI搜索时代,官网不只被"搜得到",还要被"答得出"。GEO与SEO的核心区别一句话:SEO管排名,GEO管AI引用。本文讲清GEO定义、与SEO的六维度对比、AI爬虫放行、llms.txt与JSON-LD配置,附20个站点验证数据和5个踩坑。