门店收银和普通互联网业务最大的不同,是网络不可靠属于常态:商场地下层信号弱、沿街门店晚高峰拥塞、连锁品牌偶尔专线中断。如果收银端把每一步操作都同步等待服务端返回,一旦断网,前台就只能停摆、顾客排队。离线优先(Offline-First)架构的目标,是让门店在断网时依然能正常开单、收银、退单,网络恢复后再把本地操作可靠地同步到中心,并把多端并发产生的冲突收敛到确定结果。本文拆解这套架构的三个核心:本地写队列、操作幂等、冲突解决。
离线优先不是"断网时先缓存、联网后直接覆盖",而是要先想清楚数据谁说了算。一个可落地的划分是:
划清这条边界,才能避免最危险的情况:离线期间门店各自改了主数据,联网后互相覆盖。交易可以离线产生,规则必须中心裁决。
离线能继续操作的关键,是收银端不直接"改最终状态",而是把用户的每个动作追加进一条只增不改的本地操作日志(Write-Ahead Queue),状态由日志回放得到。这样断网时操作照常入队,联网后按顺序重放即可。
一条操作记录至少包含这些字段:
{
"opId": "S018-T03-1725955200001-0007",
"type": "ORDER_CREATE",
"payload": { "items": [], "payChannel": "cash" },
"baseVersion": 4012,
"status": "PENDING",
"createdAt": "2026-09-10T14:00:00+08:00",
"retryCount": 0
}设计要点:
同步调度器在网络恢复后扫描 PENDING 操作,严格按 createdAt 顺序逐条上报,单条失败不阻塞无关操作:
async function drainQueue() {
const pending = await queue.getByStatus("PENDING", { order: "createdAt asc" });
for (const op of pending) {
try {
const res = await api.post("/sync/op", op, {
headers: { "Idempotency-Key": op.opId },
timeout: 8000
});
await queue.mark(op.opId, res.accepted ? "ACKED" : "REJECTED", res);
} catch (e) {
if (isNetworkError(e)) break; // 网络仍不通,保留队列下次再传
await queue.mark(op.opId, "REJECTED", { reason: String(e) }); // 业务拒绝转人工
}
}
}弱网下"请求其实成功了、但响应丢了"非常普遍,客户端会重试,因此服务端必须对同一 opId 幂等。幂等不能只靠数据库唯一键,还要区分"首次处理"和"重复回放":
-- 幂等表记录每个操作的处理结果
INSERT INTO op_idempotent(op_id, op_type, state, result_json, created_at)
VALUES (:opId, :type, 'DONE', :result, NOW())
ON CONFLICT(op_id) DO NOTHING;
-- 影响行数为 1 才执行业务;为 0 说明处理过,直接返回上次结果几个容易忽略的点:
离线多端并发必然产生冲突,关键是为不同数据预设确定性的解决规则,而不是弹出"谁覆盖谁"让人现场决定。常见三类策略:
冲突类型 | 推荐策略 | 说明 |
|---|---|---|
主数据多版本 | 中心版本优先(Last-Writer-Wins by version) | 本地只读,冲突时直接采用更高版本,无歧义 |
库存并发扣减 | 原子条件裁决 | 带 |
同一单据被两端修改 | 操作变换 / 字段级合并 | 收银端改了支付方式、后台改了备注,按字段合并;同字段冲突转人工 |
库存是最典型的场景。离线时本地按缓存余量做"软预占",同步时由中心用一条条件语句做最终裁决:
UPDATE stock
SET available = available - :qty, version = version + 1
WHERE sku_id = :skuId AND store_id = :storeId
AND version = :baseVersion AND available >= :qty;影响行数为 0 表示版本已变或余量不足,这笔离线预占不能成立,需要挂起并提示处理(换货、退款或调货),而不是静默成功。绝不能让两个离线终端各自把最后一件商品都卖出去,再在对账时才发现超卖。
对于金额、退款这类高风险冲突,原则是"宁可挂起转人工,也不自动合并":自动合并只适用于无歧义的主数据,涉及钱的冲突必须留痕、冻结、人工裁决。
离线优先没有银弹,通用骨架是"操作日志化、传输幂等化、冲突规则化":第一版就应把本地持久化队列、业务幂等键、库存原子裁决、冲突分级这四件事做进去,它们事后补做的代价远高于一开始就规划。技术选型上,Web 收银端可用 IndexedDB 配合 Service Worker 做本地持久化与请求代理,桌面端可内嵌 SQLite;同步通道建议区分"实时增量"与"补偿全量校验"两条,前者保及时、后者保正确。验证阶段务必做故障注入:主动拔网连续开单、两台终端同时抢最后一件商品、同步中途杀进程再重启,确认队列不丢、不重、冲突都能收敛到确定结果。
门店系统的可靠性,很多时候不取决于网络好时跑得多快,而取决于网络差时能不能不丢单、不超卖、恢复后能自动对齐。把用户动作沉淀成可重放的操作日志,用业务幂等键兜住不可靠的网络,再用分级的冲突策略把并发收敛到确定结果,离线优先的收银体系就能在最糟糕的网络环境下依然稳得住。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。