Skip to content

抓单三重保险(Webhook + 轮询 + 哨兵)

I now have a complete, code-verified picture of all three layers, their schedules, state machines, and failure modes. I have everything needed to write the analysis.


核心原理深挖:webhook + polling fallback + 漏接哨兵 三重保险(铁则4)

忠于程式码。本文所有檔案路径、cron 排程、状态字串、阈值、SQL 条件都对着实际程式码核过。下方先讲「这三层在防什么」,再逐层拆原理/数学/状态机,最后标出与 6/21 真实写入 相关的正确性风险,结尾给非技术白话总结。


0. 这三层到底在防止什么错误(一句话)

防超賣。CC WMS 推给平台的可卖量公式(铁则3,cc-wms/src/services/ledger.ts):

broadcast_qty = max(0, wms_physical − pending_orders − dc_reserved − safety_buffer)

其中 pending_orders = 已收到、但既有 WMS 还没用 PDA 扫码扣减的订单数量。只要一笔订单「真的卖出去了,但 CC WMS 不知道」,pending_orders 就少算,broadcast_qty 就偏高,平台就会把已经没有的货再卖一次 = 超賣。

所以三层保险全部围绕同一个目标:确保「平台上每一笔真实订单」都会变成 CC WMS 里的一行 orders 记录(佔住 pending),而且生命周期(新单→出货→结算 / 取消→释放)不会卡死。

  • 第一层 webhook:平台一有订单/状态变化,秒级推过来 → 立即入帐。
  • 第二层 polling fallback:每 5 分钟(Makro 随机 3–10 分)主动去平台「对一次帐」,补 webhook 漏接的。
  • 第三层 漏接哨兵 + 舊单巡檢:前两层都失手时的最后一张网——用「实体仓库的撿货单」反查「API 收到的订单」,以及定时回头扫卡死的旧单。

三层是冗余(任一层够好就不超賣),不是串联(不是缺一不可)。下面逐层拆。


1. 第一层:Webhook(秒级即时入帐)

1.1 Takealot webhook — cc-wms/src/routes/webhooks.ts

端点:POST /api/webhooks/takealot/:account:account 是帐号名,多帐号各自一条 URL)。

签名验证(防伪造)verifyTakealotWebhook(url, body, sig, secret),公式是 base64(hex(HMAC-SHA256(secret, url+body)))。每个 Takealot webhook 自动配一把独立 secret,存成 Worker secret TAKEALOT_WEBHOOK_SECRET_<帐号大写>,验不过回 401。没有签名的请求进不来(防别人裸发「取消」把货放回去卖)。

事件分流靠官方 header X-Takealot-Event,正规化(转小写、去掉空格/底线/连字号)后用「包含」比对(程式注解记了线上实测真实值是 new_leadtime_order_item_v2 / new_drop_ship_order_item_v2 / sale_status_changed,跟文档写的显示名对不上,所以用包含容错):

  • leadtime 新单processIncomingOrders 即时入庫佔 pending(你自己仓出货才扣)。
  • drop ship(DC 代发) → 只回 200、不入库不预留(货由 Takealot DC 直接出,本仓不动)。
  • sale_status_changed → 用 classifyTakealotSale()(与轮询同一个分类器,单一真相)判生命周期:
    • reserve(New Lead Time Order→new / Draft Shipment→batched)→ syncOrderReserveStatus,待处理↔备货中切换,两者都计 pending、broadcast 不变、免重算
    • ship(Confirmed Shipment / Shipped Shipment)→ markOrderShipped,移出待处理但仍计 pending,等 WMS 上传才真正释放。
    • cancel(前缀 Cancelled*Returned)→ processCancellations 释放预留 + enqueue 重算。
    • ignore:DC 代发三状态(Preparing for Customer / Shipped to Customer / Inter DC Transfer)静默忽略;没见过的状态记 log(unknown:true),但不入库、不释放(保守,宁可少动不误放)。

P0-4 自愈补建(webhooks.ts:239–305,这是 webhook 层自带的小型 fallback):如果收到「状态变更」却发现这笔订单根本不存在(说明「新单 webhook 漏接了」),不会默默落空——它会立刻拿 order_id 单笔回查 /sales 把订单补建进来(rebuildMissingTakealotOrders),用 INSERT OR IGNORE 幂等。这是为了堵「新单漏接 → 后续状态变更全部 UPDATE 落空 → 要等 5 分钟轮询才补 → 这 5 分钟有超賣窗」。取消事件补建(订单从没入库=从没佔 pending,补回再取消是净零,没意义)。

1.2 Amazon FBM/FLEX 即时 — cc-wms/src/routes/webhook-amazon.ts + amazon-notification.ts

这一条不是 Amazon 直连,而是:Amazon ORDER_CHANGE → AWS SQS → 我们自己的 Lambda 转发 → POST /api/webhooks/amazon/order-change

鉴权:Lambda 是自己人,用共享密钥 header X-CC-Webhook-Secret,跟 AMAZON_WEBHOOK_SECRET常数时间比对timingSafeEqual,长度不同直接 false,防 timing attack 猜密钥)。没配密钥回 503,密钥错回 401。

解析与分流parseOrderChangeNotification,纯函数)——这是理解「webhook 会漏什么」的关键状态机

FulfillmentType + SKU 尾缀分四类:

  • MFN(=FBM,自有仓发)→ 全部品项入帐本。
  • AFN 且 SKU 尾缀 -SF(=Seller Flex,Amazon 报 AFN 但货从自有仓出、与共享池同一批)→ 入帐本,variant='FLEX'
  • AFN -SF(真 FBA,货早在 Amazon 仓)→ fba_noted只记一笔不入帐本(不影响本地库存,正确)。
  • 其余 → ignored

订单状态映射(classifyOrderStatus):

  • Unshipped/PartiallyShippedreserve_new(status='new',即时佔池)。
  • Shippedreserve_shipped(status='shipped',仍计 pending)。
  • Canceled/Unfulfillablerelease(释放)。
  • Pending/UpComing/PendingAvailability/InvoiceUnconfirmedignore(等真正可行动再处理)。

去重:不另建表。靠 platform_order_id = ${orderId}-${sku}(与轮询同一复合键)+ 数据库 UNIQUE 约束 + INSERT OR IGNORE。SQS 可能重送/乱序,但既有 processIncomingOrders 对订单身分幂等,已 shipped/cancelled 的单不会被晚到的旧通知打回头。

⚠️ 这里藏着 ADR-049 修过的一个大坑(与 6/21 高度相关,下文 §5 详述):SF 单在 Amazon Orders API 从下单起长期停在 Pending,而 Pending 阶段的 ORDER_CHANGE payload 不带品项明细 → 看不到 -SF 尾缀 → 被当 fba_noted 跳过(实证幽灵单 171-6446757 通知只记 {"kind":"fba_noted"})→ SF 单从下单到隔日才显形、这段时间不佔池 = 每天约 20 件 × 最长 30h 的超賣窗。webhook 层对 SF 的 Pending 阶段是「天然盲的」,这是为什么第二层轮询必须补 Pending(见 §5)。

1.3 Makro — 设计上没有 webhook(webhooks.ts:324–330)

南非 Makro 跑 Flipkart Commerce Cloud(FCC),其 webhook 能力/格式未确认(裁决 D-06),所以故意不做、不逆向。Makro 的取消/出货完全靠第二层轮询(订单从 pending 桶里消失 = 已撿货,ADR-044)。这意味着 Makro 没有第一层,第二层就是它的第一层——6/21 要特别注意 Makro 的轮询是否健康(见 §5)。


2. 第二层:Polling Fallback(每 5 分钟主动对帐)

排程在 cc-wms/wrangler.tomlcrons = ["*/5 * * * *", "* * * * *", "23 * * * *", "47 2 * * *"],分流在 cc-wms/src/index.tsscheduled()

Cron跑什么为什么独立
*/5 * * * *Takealot 轮询 + Amazon 轮询 + 出货单同步 + 漏接哨兵(白天 gated)主链路
* * * * *Makro tick(每分钟只是闹钟,真打 Makro 由随机 3–10 分钟抖动门控制)独立预算,免被 Amazon 重活饿死(2026-06-09 实证 Makro 被饿死 35 分钟)
23 * * * *Takealot 舊单巡檢 + Amazon 卡单巡檢 + 连接健检(第三层,见 §3)独立 invocation 不抢子请求
47 2 * * *数据保留清除(ADR-053,与本主题无关)

2.1 轮询的核心:水位游标 + 对帐式拉取

每个平台存一个 last_poll_<platform> 时间戳。每轮拉「上次水位之后更新的订单」,处理完把水位推进到 now。首次(无水位)回看 7 天。

Takealot(pollTakealot:多帐号 round-robin(每轮只轮一个帐号,游标 poll_rotation_takealot +1),每帐号各自的 last_poll_takealot_<account> 水位(避免共享时间戳漏单——之前只轮第一个帐号,导致 MIUI 没 fallback)。pullSalesReconcile(since) 一次产出三样:新单(入庫预留)、已离仓(标 shipped)、终态(释放预留)。它是 webhook 的 API 后备(鐵则4 / webhook 文档 L98)。

Makro(pollMakro:两道闸——

  1. 抖动门(反爬):makro_next_poll_at 控制真正打 Makro 的间隔为随机 3–10 分钟(cron 每分钟只是闹钟),让 Flipkart 看到的请求间隔不固定。
  2. 指数退避(418/429 保险阀):吃到 HTTP 418(Flipkart Gatekeeper 反爬)/429 → 退避 5→10→20→40→60 分钟封顶,任一次成功归零。pullOrdersReconcile 用「已知单离开 pending 桶 = 已撿货」标 shipped(ADR-044),仍计 pending,等下次 WMS 上传由 settle 路径 C 核销。

2.2 Amazon 轮询的「同秒爆发」边界游标(poller.ts:101–192,computeAmazonPollCursor

这是个精巧但脆弱的纯函数,值得讲清楚,因为它直接关系 6/21 的漏单风险。

问题:撿货单生成会在「同一秒」盖一大批 SF 单。pullOrders 每轮封顶 15 张、靠时间水位推进。旧逻辑在「同秒单数 > 15」时用「水位 +1 秒」防卡死,却把该秒超出 15 张的部分永久跳过(2026-06-13 FLEX 对帐实证 135947/131345 两张在庫单从没进 CC = 超賣)。

新解(数学)

  • 若本轮未封顶capped=false)→ 窗口已全处理完 → 水位推到 NOW、清空边界游标。
  • 封顶→ 水位停在该批最大更新时间(边界秒 watermark),并把「该秒已处理的订单号」记进 last_poll_amazon_boundary{ts, ids[]})。下轮 since=该秒excludeIds=该秒已处理 ids,于是抓「同秒剩下的单」,整秒处理完(某轮不再封顶)才前进。重复由 INSERT OR IGNORE 吸收。

不变式:只要同一边界秒还没整秒抓完,水位就不前进,所以不会跳过任何单。这是 loss-free 的。

2.3 为什么 webhook + polling 是冗余而非重复

两条路写的是同一个复合键 platform_order_id(Amazon=orderId-sku,Takealot=order_item_id)+ UNIQUE 约束 + INSERT OR IGNORE。所以:webhook 先到就 webhook 入,轮询先到就轮询入,另一条到了被幂等吸收。生命周期 UPDATE(reserve/ship/cancel)都有状态守卫(已 shipped/cancelled 不被晚到的旧信号打回头)。这保证了「两路并发」不会双扣或互相覆盖。


3. 第三层:漏接哨兵 + 舊单巡檢(最后一张网)

前两层防的是「平台报了、CC 漏接」。但有两类错误它们防不住,需要第三层:

3.1 舊单巡檢(poller.ts,cron 23 * * * *)— 防「卡死的旧单」

防的错误:信号发生在捕捉机制上线之前或被漏接的活单,没有任何退场通道(实证 422988401:6/7 客取消,但取消捕捉 6/8 才上线,轮询只看新窗口不回头 → 永久卡「待处理」佔 1 件库存,压低 broadcast=少賣)。

  • sweepStaleTakealotOrders:每小时挑最旧一批 >48h 的 new/batched 单,按(帐号, 下单日 SAST)分组用 /sales 日期窗重查 → 取消则释放、已离仓标 shipped、DC 代发三状态KNOWN_NONRESERVEignore && !unknown)→ 释放(货在 Takealot 网内由 DC 出、本仓永不出货,留着=永久幽灵)。日期窗找不到 → 2026-06-15 加了订单号点名查备援pullSaleStatusesByOrderId,治 DC 代发+晚到 webhook 的 ordered_at 与 Takealot order_date 日期偏移幽灵单);还找不到才记 notFound(保守不动)。预算每轮 ≤3 组、每组 ≤5 页。
  • sweepStaleAmazonOrders(ADR-048):每小时挑 >48h 的 FBM/FLEX(排除真 FBA 与虚拟单)活单,getOrder by ID 无时间窗逐张问 OrderStatus:Shipped/PartiallyShipped→标 shipped、Canceled/Unfulfillable→释放、其余 stillOpen 不动(并维护 awaiting_payment 旗标)。每轮 ≤15 张不同订单号。
  • 关键修正(ADR-049):cutoff 比较一律包 datetime(ordered_at)——裸字串比较时 ISO 的 'T'(0x54) > 空格(0x20),会让同日订单恒大于 cutoff 永不入选,「48h」实变 48–72h。

巡檢方向永远偏「保守不超賣」:看不懂的状态一律不动。它清的是「少賣型卡死」,不是直接防超賣,但卡死单堆积会让 broadcast 长期偏低。

3.2 漏接哨兵(cc-wms/src/services/missed-order-sentinel.ts)— 防「卖了但 CC 完全没这笔单」

这是最外圈、唯一不依赖平台 API 回报的保险(规格 docs/superpowers/specs/2026-06-13-missed-order-sentinel-design.md,ADR-057)。

核心思想:用舊倉庫的撿货单(实体真相:货真的被撿出去了)反查 API 订单。如果「撿货量 > API 收到量」,说明很可能有订单 API 整笔漏接(前两层都失手),是疑似超賣。它只警示、绝不改库存、绝不改单。

调度:併入 */5 cron,但内部 gated——maybeRunSentinel 只在 SAST 08–17、且距上次≥(30−2)分 才真跑,避免新增 cron 触发器、夜间 😴 休息。

数学/状态机(纯函数 computeSentinelRows,30 个验证坑都在这):

gap = picked − booked            // 撿货已出 − API 收到(仅 amzfbm / amzsf / makro)
gap ≤ 0 → 对上(绿),不出 row
gap > 0 → 嫌疑 row
  • 平台键amzfbm=(amazon,FBM)、amzsf=(amazon,FLEX,⚠️ SF='FLEX' 不是 'SF')、makro=(makro,*)。故意排除 Takealot(po-/alexshop)——不是因为「不在本地撿」(那是错的、已更正),而是 po- 混 leadtime 客户单 + DC 补货、无法分离,DC 补货撿货量会灌进 picked 却不在 booked → 每天大量假漏接洪水(哨兵只读不扣库存,是假警报、非双扣超賣);Takealot 已由「出货镜像 + FIFO settle」端到端追踪。也排除真 FBA/DC/虚拟单。
  • 累计可疑升级(H2)firstSeen 跨「短暂归 0」不重置,必须连续 clearStreakK=3 次干净才解除。aged = (now − firstSeen) ≥ 72h → 红灯(durRed);未到 72h → 黄灯。
  • 取消只註记不相减(H3):近窗有取消单 → 只加一句註记「可能取消后仍出货(退货回流)」,永不下调 severity(绝不盖掉真超賣红灯)。
  • 窗口纪律:撿货看回 5 天(> 72h 红灯门限 + 2 天缓冲,否则真漏接的撿货在转红前滑出窗口被当干净清掉,SENT-CORE-1);booked 看回 14 天(订单先于撿货,非对称窗)。
  • degraded(连不到仓库)runSentinel 抓撿货失败 → 标 wmsUnreachablemarkSentinelDegraded 把卡片转 ⚪「失联」(不更新 lastScanAt,前端自然过期),绝不掩盖根因、绝不误导业主去查别的
  • truncated(撿货明细超单轮抓取上限 20):本轮覆盖不全 → 前端绝不显示绿(燈号至少 warn,F1),下轮自动接上。

已知盲区(诚实标注,ADR-057):①舊倉庫订单由人工从 Takealot 后台输入、与 CC /sales 各走各的 → leadtime 单若 CC 漏抓却被人工 po- 撿出 → 出货镜像与哨兵皆看不到 → 漏网(守法=源头体检查 order_status_unknown + probeSales,不建 po- 监控)。②pending_mapping(没对到 wms_code 的单)会被当 gap 列又进 skuNotice = 看似双重漏接(实为未对照,设计使然)→ 根治靠自动抽码对照(#6)。③多帐号:若未来第二帐号(如 Marasy)订单进 CC,必须把前缀加进 CHANNEL_TO_PLATKEY 且比对键加 account_name,否则同 code 跨帐号超賣互抵(SENT-COMP-8)。


4. 三层如何合成「防超賣」(整体状态机)

一笔平台订单的理想生命周期,三层在不同节点补位:

平台下单
  ├─[L1 webhook 秒级] → orders(status=new, 佔 pending)         ← 正常路径
  ├─[L1 漏接?] → [L2 轮询 ≤5min] 对帐式拉取补入                  ← webhook 漏接
  │              └─[L2 也漏? Amazon Pending 盲区/同秒爆发] 
  │                 → [L3 哨兵] 撿货>API 报黄/红警示             ← 前两层都失手
平台出货
  ├─[L1 ship 信号] markOrderShipped (仍计 pending)
  ├─[L1 漏?] → [L2 轮询] 离桶/Confirmed 判 shipped
  │            └─[L2 漏? SF不发即时通知/旧单>72h窗外]
  │               → [L3 舊单巡檢 hourly] getOrder/日期窗 补判
平台取消
  ├─[L1 cancel(终态)] processCancellations 释放 pending
  ├─[L1 漏?] → [L2 轮询] 终态释放
  │            └─[信号早于捕捉机制上线 → 永久卡死]
  │               → [L3 舊单巡檢] 取消/DC代发 释放幽灵单
WMS 上传 xlsx → settle 路径 A~E 真正释放 pending

失败方向设计原则贯穿三层:所有「看不懂/查不到」一律保守不动(宁可多扣 pending = 少賣),绝不主动释放。少賣可恢复(下次对上就好),超賣不可恢复(货真没了)。


5. 与 6/21 真实写入相关的正确性风险(重点)

6/21 是第一次真实推送虚拟库存到线上平台(之前可由 ADR-035 全局只读模式/ADR-036 空系统硬保险挡住)。三层保险此前都在「只读对帐」模式下验证过,但真实写入会让「pending 算错 → broadcast 算错 → 真的推一个错数字上去」。以下是按风险高低排的、与三层直接相关的正确性风险:

🔴 高:Amazon SF 单 Pending 阶段的入帐依赖单一路径,没有第三重独立兜底

  • 机制:SF 单 webhook 在 Pending 阶段结构性盲(payload 无品项明细,被当 fba_noted,§1.2)。ADR-049 的修复是让第二层轮询补 PendingpullOrders 的 OrderStatuses 加 Pending、只收 -SF、即时佔池)。所以 6/21 当天 SF 单佔池 100% 依赖「Amazon 轮询正常跑 + Pending 分支正确」
  • 风险点:如果 */5 cron 当天因为资源限制/部署窗口/Amazon API 5xx 连续失败,SF 单会有最长约 30h 隐形窗,期间 broadcast 偏高 → 真实推送时把不存在的 SF 库存推上去 = 超賣。哨兵(L3)虽能事后报警,但哨兵只白天每 30 分跑、且只「警示不阻止推送」——它不会挡住已经偏高的 broadcast 被推出去。
  • 6/21 必做:盯 poll_status_amazon 是否每 5 分钟绿;确认 last_poll_amazon 在前进;SF 单先小批验证再放量。

🔴 高:Makro 没有 webhook(L1 缺位),整个防超賣只剩 L2 单层

  • 机制:Makro 取消/出货只靠轮询「离 pending 桶」判定(§1.3 / ADR-044)。而 Makro 轮询本身受反爬退避约束:吃 418/429 会退避最长 60 分钟静默不打 Makro。
  • 风险点:6/21 如果 Makro 又被 Flipkart 418(记忆 project_makro-connector-state 记载:Nate 登 Makro 后台会踢掉 session → 需重抓 cookie;机房 IP 会被挡,靠 VPS relay 156.155.253.101 绕过),轮询停摆期间 Makro 卖出不入帐、卖出也不释放——单层失守就直接影响 broadcast 正确性。哨兵能覆盖 Makro(amzsf/makro 在监控范围),算半层兜底,但同样只警示。
  • 6/21 必做:确认 Makro session 新鲜(Nate 别在测试窗口登后台)、VPS relay 活着、poll_status_makro 绿、backoff=0;目前还有约 5 单 pendingMapping 待补 SKU 对照。

🟡 中:哨兵/巡檢「只警示/只释放,从不阻止推送」——它们不是推送闸门

  • 机制:三层保险修的是 pending 的准确性,但推送闸门是另一套(ADR-035 全局只读、ADR-036 空系统硬保险、ADR-037 push-mode、min_price CHECK 等,在 executePlatformPush)。哨兵报红不会自动暂停推送。
  • 风险点:6/21 如果哨兵在 09:00 报「某 SKU 撿货 > API = 疑似漏接 8 件」,broadcast 已经按偏高的数字算好、且 */5 cron 会自动推。人没在看屏幕的话,警示形同虚设。
  • 6/21 建议:真实写入第一天,考虑先用 push-mode=manual(ADR-037:系统照算 broadcast 但不自动推,攒成待推清单等人按「立即推送」),让哨兵/巡檢的警示有人确认后再 flush。这把「自动推」降级为「人确认后推」,给三层保险一个真正能拦截的关口。

🟡 中:Amazon 同秒边界游标在「持续高峰」下会拖慢前进,但 6/21 测试量小通常无碍

  • 机制:§2.2 的边界游标在「同秒 > 15 单」时停在边界秒逐轮消化。逻辑是 loss-free 的,但如果持续有同秒大批量(撿货单连续盖单),水位前进会变慢,新单入帐有几轮延迟。
  • 风险点:6/21 测试单量小,触发概率低;但若 Nate 同时跑大批撿货单做测试,可能观察到「新单晚几分钟才显示」。这是延迟非丢失,不是超賣,但会让现场判断「是不是漏了」时困惑。

🟢 低(但要知道):webhook 的 P0-4 补建 / 哨兵盲区是已知诚实缺口

  • Takealot leadtime 单若 CC /sales 漏抓(新 sale_status 字串被丢进 order_status_unknown)却被人工撿出,三层全看不到(ADR-057 盲区①)。守法是源头体检,不是再加一层。6/21 前可跑一次 probeSales + 查 order_status_unknown 稽核,确认没有新状态被默默丢弃。
  • 哨兵 pending_mapping 会显示成「看似漏接」(设计使然),6/21 看到黄灯先别慌,先确认是不是未对照 SKU。

6. 给非技术读者的白话总结

这三层保险是干嘛的? 防止「同一件货卖两次」(我们叫超賣)。系统推给平台的「可卖数量」会自动减掉「已经卖出但仓库还没扫码确认的订单」。只要有一笔订单系统不知道,可卖数量就会算多,平台就会把没有的货再卖一次。三层保险就是用三种不同的方法,确保「平台上每一笔订单」都不会从系统眼皮底下溜走。

三层各是什么(用收快递比喻):

  1. 第一层 webhook(门铃):平台一有订单,立刻按门铃通知系统,秒级入帐。最快,但门铃偶尔会坏(漏按)。
  2. 第二层 轮询(每 5 分钟去信箱看一次):不管门铃响没响,系统每 5 分钟主动去平台「对一次帐」,把门铃漏掉的补回来。Makro 平台没有门铃,全靠这层。
  3. 第三层 漏接哨兵(盘点实物):前两层都失手时的最后一招——直接看「仓库实际撿出去多少货」,跟「系统收到多少订单」对一对,撿得比收到的多,就亮黄灯/红灯报警。它只报警、不动库存、不改单。还有一个「舊单巡檢」每小时回头扫一遍卡住没动静的旧订单,自动清掉。

设计的脾气:遇到看不懂、查不到的情况,三层一律「宁可少卖也不多卖」——少卖明天对上就好,多卖货真的没了。

6/21 第一次真上线要盯什么(白话):

  • Amazon 的 Flex(自有仓代发)订单最危险:平台对这种单的「门铃」天生是哑的,全靠第二层每 5 分钟去对帐。所以那天一定要确认「Amazon 每 5 分钟的对帐」是绿灯、没卡住。
  • Makro 最脆:它没门铃只有一层,而且容易被平台反爬虫挡(一挡就最长 60 分钟不去对帐)。那天别去登 Makro 后台(会把系统的登入踢掉),并确认 Makro 是绿灯。
  • 最重要的一条:三层保险只负责「把数字算准 + 报警」,它们不会自动拦住推送。所以建议第一天把推送改成「手动确认模式」——系统算好可卖数量但先不自动推,等人看一眼(特别是看哨兵有没有报红)再按「推送」。这样万一哪层失手,还有人把最后一道关。

关键文件路径(供后续整理教学网站)

  • 第一层 webhook:C:\Users\langl\Downloads\CC WMS\cc-wms\src\routes\webhooks.ts(Takealot,含 P0-4 补建)、C:\Users\langl\Downloads\CC WMS\cc-wms\src\routes\webhook-amazon.ts + C:\Users\langl\Downloads\CC WMS\cc-wms\src\services\connectors\amazon-notification.ts(Amazon SQS→Lambda 链路,含 SF/FBA 分流状态机)
  • 第二层 polling:C:\Users\langl\Downloads\CC WMS\cc-wms\src\services\poller.tspollTakealot / pollAmazon + computeAmazonPollCursor 边界游标 / pollMakro 抖动+退避)
  • 第三层 哨兵+巡檢:C:\Users\langl\Downloads\CC WMS\cc-wms\src\services\missed-order-sentinel.ts(哨兵纯函数 computeSentinelRows + I/O 编排 runSentinel/maybeRunSentinel)、巡檢 sweepStaleTakealotOrders / sweepStaleAmazonOrders(同在 poller.ts)
  • 调度:C:\Users\langl\Downloads\CC WMS\cc-wms\src\index.tsscheduled(),cron 分流)+ C:\Users\langl\Downloads\CC WMS\cc-wms\wrangler.tomlcrons 第 74 行)
  • 路由:C:\Users\langl\Downloads\CC WMS\cc-wms\src\routes\sentinel.ts(前端读哨兵结果)
  • 去重/分类核心:C:\Users\langl\Downloads\CC WMS\cc-wms\src\services\order-processor.tsprocessIncomingOrdersINSERT OR IGNORE 幂等)、C:\Users\langl\Downloads\CC WMS\cc-wms\src\services\connectors\takealot.tsclassifyTakealotSale 状态分类器,webhook 与轮询共用)
  • ADR 依据(C:\Users\langl\Downloads\CC WMS\docs\13-決策記錄.md):ADR-040/043/044/045(出货镜像与 settle)、ADR-046(Takealot 舊单巡檢)、ADR-048(Amazon 卡单巡檢)、ADR-049(SF Pending 即时入帐——与 6/21 最相关的超賣窗修复)、ADR-052/054(每日对帐兜底)、ADR-057(撿货单档名+哨兵排除 Takealot 的理由+已知盲区)
  • 哨兵规格:C:\Users\langl\Downloads\CC WMS\cc-wms\docs\superpowers\specs\2026-06-13-missed-order-sentinel-design.md

(注:铁则4 的「webhook 必须加 polling fallback」散见各 ADR 与 CLAUDE.md,无单一独立 ADR 编号;ADR-046/048/049 是其在各平台的具体落地。)