首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >门店断网还能收银:本地队列、断网续传与冲突对账的离线优先设计

门店断网还能收银:本地队列、断网续传与冲突对账的离线优先设计

原创
作者头像
数字化落地笔记
发布2026-09-13 16:33:35
发布2026-09-13 16:33:35
640
举报

导读

线下门店和纯线上业务最大的不同,是网络不可靠这件事每天都在发生:商场地下一层信号差、运营商线路临时故障、高峰期路由器拥塞。如果收银系统一断网就瘫痪,顾客排着长队结不了账,损失的是实打实的成交。一套合格的门店系统应当做到"断网能继续营业、恢复后自动对账、数据最终不错不乱"。这篇文章拆解离线优先(Offline-First)的收银设计:本地持久化队列、断网续传、客户端生成幂等单号、恢复后的冲突检测与对账,并复盘五个踩坑。

一、先定原则:离线不是异常分支,而是默认要支持的状态

很多系统把"没网"当成异常直接报错,离线优先的思路恰好相反:默认网络会断,核心交易在本地就能闭环,网络只是用来同步的。门店收银要保证三件事:

  1. 断网期间能正常开单、收银、打印小票,顾客无感知;
  2. 本地数据先落盘,应用崩溃、设备重启也不丢单;
  3. 网络恢复后自动续传,服务端幂等接收,并通过对账发现和处理冲突。

二、第一层:本地优先写入,交易先落本地库

收银端(Pad/POS)内置一个本地数据库(如 SQLite/IndexedDB),开单时不等待服务端,先写本地、立即返回成功,同时给每笔交易打上同步状态标记。

代码语言:javascript
复制
// 交易状态机:LOCAL_ONLY(仅本地) -> SYNCING -> SYNCED / CONFLICT
async function createOrderLocal(cart, payment) {
  const order = {
    orderNo: genLocalOrderNo(),   // 客户端生成全局唯一单号(见第三节)
    items: cart,
    payAmount: payment.amount,
    payChannel: payment.channel,
    status: 'LOCAL_ONLY',
    createdAt: Date.now(),
    deviceId: DEVICE_ID,
    version: 1,
  };
  await localDb.orders.put(order);     // 先落本地,事务保证持久化
  printReceipt(order);                 // 立即出票,不阻塞于网络
  syncQueue.enqueue(order.orderNo);    // 入同步队列,在线就立刻尝试上传
  return order;
}

关键是写入本地要用真正的事务落盘,而不是只写内存,否则 POS 突然断电,刚收的钱就没记录了。

三、第二层:客户端生成幂等单号,续传多少次都不重复

离线场景下单号不能由服务端发(断网时拿不到),必须客户端生成全局唯一 ID,并且让服务端以这个单号做幂等键,这样断网续传、手动重试、超时重发都不会产生重复订单。

代码语言:python
复制
import time, uuid

def gen_local_order_no(store_id, pos_id):
    # 门店号+终端号+毫秒时间+随机段,全局唯一且自带业务信息
    ts = time.strftime('%Y%m%d%H%M%S')
    rand = uuid.uuid4().hex[:6].upper()
    return f"{store_id}{pos_id}{ts}{rand}"

def server_ingest(order):
    # 单号唯一索引兜底:重复上传直接返回已存在,不再插第二笔
    existed = db.query("SELECT status FROM orders WHERE order_no=%s", order['order_no'])
    if existed:
        return {'code': 0, 'orderNo': order['order_no'], 'duplicated': True}
    db.insert('orders', order)
    return {'code': 0, 'orderNo': order['order_no'], 'duplicated': False}

注意:支付状态要和下单分开对账。离线时"现金"可以直接确认;"扫码支付"在断网时无法真正向支付通道确认,应当只记录顾客付款意图,联网后以支付平台的实际流水为准,避免出现"小票打了但钱没到账"。

四、第三层:同步队列与断网续传

本地维护一个同步队列,按先进先出上传,网络恢复自动续传,失败指数退避,并实时反馈同步进度给店员。

代码语言:javascript
复制
class SyncEngine {
  async flush() {
    if (!navigator.onLine || this.running) return;
    this.running = true;
    const pending = await localDb.orders.where('status').equals('LOCAL_ONLY').toArray();
    for (const order of pending) {
      try {
        await localDb.orders.update(order.orderNo, { status: 'SYNCING' });
        const res = await api.post('/orders/sync', order);
        await localDb.orders.update(order.orderNo, {
          status: 'SYNCED', serverAckAt: Date.now(),
        });
      } catch (e) {
        await localDb.orders.update(order.orderNo, { status: 'LOCAL_ONLY' });
        await this.backoff();   // 失败退避,等下一轮,不阻塞后续可成功的单
      }
    }
    this.running = false;
  }
  start() {
    window.addEventListener('online', () => this.flush());
    setInterval(() => this.flush(), 15000); // 兜底轮询
  }
}

同步要做到"单条失败不阻塞整批":某一单因为数据问题传不上去,先跳过它继续传后面的,最后把问题单单独拎出来处理,避免一单卡死全天营业数据。

五、第四层:冲突检测与恢复后对账

离线期间多个终端可能各自开单,恢复后需要一套对账机制兜底,而不是假设一定不冲突:

  1. 单号唯一性对账:客户端幂等单号 + 服务端唯一索引,重复上传自动收敛;
  2. 库存/班次对账:离线扣减的库存、收银班次的应收金额,联网后与服务端汇总比对;
  3. 差异清单:把"本地有服务端无、服务端有本地无、金额不一致"的单列成差异表,由店长确认处理。
代码语言:sql
复制
-- 恢复后拉取服务端本班次订单,与本地做全量比对
SELECT o.order_no, o.pay_amount, l.pay_amount AS local_amount
FROM orders o
LEFT JOIN local_upload_log l ON o.order_no = l.order_no
WHERE o.shift_id = ? AND o.created_at >= ?
HAVING o.pay_amount <> IFNULL(l.pay_amount, -1);

对账不追求"零人工",而是把可能出错的极少数单精确定位出来,让店员只处理异常,而不是逐单核对。

六、离线期间的体验边界与数据安全

离线能营业不等于什么都能离线,必须给店员清晰的边界提示,避免误用:界面上要常驻"当前离线,数据保存在本机,联网后自动同步"的状态条,让店员知道现在是离线模式;离线期间不可用的能力(如核销需要服务端校验的线上券、查询会员实时余额)要明确置灰并说明,而不是让店员操作到一半才失败。本地落库的交易数据涉及金额和顾客信息,应当加密存储并绑定设备,POS 退出登录或交班后清理敏感缓存,防止设备丢失导致数据泄露。把"哪些能离线、哪些不能、离线数据存哪安不安全"在产品层定义清楚,离线优先才不会在真实门店里制造新的混乱。

七、五个真实踩坑清单

  1. 交易只写内存不落盘:POS 断电重启后离线单全丢。必须用本地数据库事务落盘,重启能恢复。
  2. 单号由服务端生成:断网时拿不到号无法开单。必须客户端生成全局唯一单号,服务端以它做幂等。
  3. 离线直接确认线上支付:断网无法核验扫码支付是否真到账,却按已收款出票。离线只确认现金等不依赖网络的方式,线上支付以联网后的通道流水为准。
  4. 一单失败卡死整个同步队列:坏单反复重试,后面正常单也传不上去。要单条隔离、失败跳过、最后集中处理异常单。
  5. 恢复后不做对账:以为同步成功就万事大吉,库存和金额差异长期积累。每次联网和交班都要自动跑差异对账,异常显性化。

结语

门店离线收银的本质,是把"网络"从交易的必经之路降级为同步工具:本地事务保证断网能营业且不丢单,客户端幂等单号保证续传不重复,同步队列保证恢复后自动收敛,对账机制把极少数冲突精确定位。这套离线优先思想同样适用于上门服务、仓储盘点、移动巡检等所有弱网现场作业场景。下一步建议在门店做一次真实的"拔网线演练":营业高峰主动断网 20 分钟再恢复,观察是否能正常出票、续传是否零重复、对账差异是否都能被自动找出,用演练结果打磨异常处理流程。

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

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

目录
  • 导读
  • 一、先定原则:离线不是异常分支,而是默认要支持的状态
  • 二、第一层:本地优先写入,交易先落本地库
  • 三、第二层:客户端生成幂等单号,续传多少次都不重复
  • 四、第三层:同步队列与断网续传
  • 五、第四层:冲突检测与恢复后对账
  • 六、离线期间的体验边界与数据安全
  • 七、五个真实踩坑清单
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档