通知系統:兩個欄位、一支排程,和一通電話

· tech

#war-story#live-commerce#notification

📑 目錄

「通知系統」四個字在教科書裡的長相:一套 notification service、一組 message queue、模板引擎、多渠道 SDK、退避重試框架。這章要講的版本是:兩個欄位、一支排程掃描、一條十秒規則,和客服的一通電話。它小得不像個系統——而它送達了 99% 的通知,剩下的 1% 也有人接。

渠道帳本:成本與嚴重度對齊

先盤點渠道。這個系統的通知走四條路,嚴重度越高、渠道越貴:

訊息嚴重度 → private reply 自動・免費・量大 得標通知+綁定 token 政策牆:一則留言 只能回一次 email 常規・內部通知 async 完成通知走這裡 Google Workspace 現成 缺點:會沉底 電話(客服) 人工・最貴・必達 最後通牒:錢要被清、 人要進黑名單之前 渠道來源:綁電話送免運 簡訊:OTP+最後催繳(一檔期只發一次) 簡訊沒催動的,才輪到電話 渠道成本與訊息嚴重度對齊;而觸達權不是接 API 就有的——它是產品換來的
三級階梯加一條旁支:越嚴重的訊息走越貴的渠道,最貴的那條靠人。

最值得停的是催付這條線,它是兩段式的。檔期尾聲,先發一次催繳簡訊——簡訊有 API,但一個檔期只用這一次,稀缺渠道省著用的紀律做到了極致;簡訊沒催動的,才進客服電話:再不付款單就要被清、人要進黑名單,這種最後通牒交給任何自動渠道都不夠——private reply 有政策牆(FB 規定一則留言只能回一次,得標通知已經用掉了),email 會沉底,必達的訊息只有電話。而無論簡訊還是電話,前提都是有號碼——所以當年做了「綁定電話送免運」的活動,很多客人綁了。用運費採購一條高觸達渠道,順便給簡訊 OTP 鋪路——觸達權是稀缺資產,它不是接了 API 就有,是產品拿東西換來的。這是我在這個系統裡最喜歡的增長設計之一。

沒有隊列的通知隊列

得標通知怎麼發?教科書會畫一條 message queue。當年的答案是:不需要隊列,因為事實表就是隊列

fbmsgtocartitem fb_user・fb_msg・cart_item・bidding_key 是否成功送訊息 重試次數 排程掃描 已結標・未成功・重試<5 FB batch API 得標通知+綁定 token 回寫欄位 成功✓ 或 重試+1 終態:成功,或重試滿 5 次——不再處理;送達 ~99%,殘量歸客服 這張表的第五個身分:訂單溯源・LWW upsert・通知隊列・投遞帳・對帳錨點
掃表就是取件、欄位就是投遞帳:兩個欄位換掉一套 message queue 加 notification service。

流程一句話講完:喊單會 upsert fbmsgtocartitem,排程掃這張表裡已結標、未成功、重試不滿 5 次的列,打 FB 的 batch API 發得標通知(附綁定 token),回頭更新成功欄位或重試次數。成功、或重試滿 5 次,就是終態——有界的 at-least-once,不會有殭屍任務永遠重試。

這個設計的好,要跟教科書版對照才顯出來:多數團隊會為這個需求上一套 MQ + notification service + 投遞狀態表——三個新元件、三處新的一致性邊界。這裡是兩個欄位:隊列就是掃表的 WHERE 條件、投遞帳就是欄位本身,而「這位客人收到通知了嗎」是一句 SQL,不是跨三個系統的追蹤。fbmsgtocartitem 至此有了五重身分:訂單溯源、LWW 的 upsert 目標、通知隊列、投遞帳、對帳錨點——一張表五個角色而不勉強,因為每個角色只讀寫自己的欄位,而它們共享同一個事實粒度:一則留言一列。

戰記:被 FB 文件坑的那次限速

這章唯一的事故。一開始發通知是一則一則打 FB API——然後被限速了,得標通知大塞車,客人在留言區問「怎麼還沒收到」,主播罵死

最嘔的是:打的量明明離 FB 文件寫的速率上限還很遠。對著文件檢查了半天,沒有超,但就是被限。最後的修法是改用 batch API 一次打包,從此穩定,送達率 99%——剩下那 1% 是無論如何都寄不出去的帳號(FB 端的因素),靠客服補。

教訓兩條:外部平台的文件是參考值,不是 SLA——真實限額只能實測,尤其當你的流量模式(直播結標瞬間的爆發)跟平台想像的不一樣時;以及「發不出去」必須是可見的狀態——表上那個成功欄位,讓 1% 的殘量有地方可查、有人可接,而不是無聲蒸發。這跟留言章「失敗略過」的教訓遙相呼應:當年在留言那頭學到的課,在通知這頭做對了。

三條小規則,省掉一個系統

這章剩下的部分,是三條各自省掉一個子系統的規則:

  • 10 秒規則。 任何超過 10 秒的操作,一律做成 async task+完成後寄 email 給操作者(例:匯出檔期訂單)。「要不要 async」從逐案的架構討論變成一條常數規則,內部使用者也學會了心智模型:久的事情,信箱見。順帶一提,內部人員登入本來就是公司 email+OTP——通知和認證共用同一條信任鏈,零密碼。
  • 催付名單不是功能,是一次查詢。 客服要打電話催付,名單哪來?後台的訂單/購物車管理頁支援可組合的花式搜尋(Django Ninja 把 filters 組起來),「快結束檔期下的未付 cart item」就是一組查詢條件。通用查詢機制做得夠好,專用清單頁的需求會自己消失——機制歸系統,政策歸人:系統給查詢能力,打給誰、先打誰,客服決定。
  • 站內通知,不做。 站內通知意味著一整套:未讀狀態、通知列表、已讀回報、推播——而 email 已經躺在 Google Workspace 裡。「還不值得自建」是這個團隊反覆展現的判斷力:抽象和基礎設施,等第二個真實需求出現再蓋。

還有一條「不通知」也值得回收:重喊被清掉舊單的客人,系統不另行通知——主播口播就是通知,系統貼心一分,直播的急迫感就漏一分;重喊後重新喊到的人,照常走得標通知。通知系統的邊界,不只是「發什麼」,也是「刻意不發什麼」。

重來會怎麼做

這是全系列重來清單最短的一章:幾乎原樣保留。兩個欄位的投遞帳、掃表的隊列、渠道分級、10 秒規則,放到今天依然是對的尺寸。真要挑,只有兩件小事:

  1. 「寄不出去」變成客服頁的預設 filter。 殘量已經可查(成功欄位在),重來把它做成一鍵——讓 1% 的名單主動出現在客服眼前,而不是等有人想到去查。
  2. 等第二個渠道再抽象。 如果 LINE、簡訊行銷這些渠道真的要加,才值得立一層通知抽象(渠道介面+統一投遞帳);在那之前,任何「通知中心」都是替想像中的需求蓋房子。

反思

觸達權是買來的,不是接來的

工程師的直覺是「接上 API 就能發通知」——實際上每條渠道都有它的牆:FB 有政策(一則留言回一次、24 小時窗口),email 有沉底,簡訊有成本。真正稀缺的不是發送能力,是對方會看到的權利。「綁定電話送免運」教我的是:觸達權要當資產經營——它可以被購買(免運換號碼)、會折舊(騷擾就失效)、要省著用(電話只留給最後通牒)。通知系統設計的第一問不是「怎麼發」,是「我憑什麼發,而他為什麼會看」

最好的基礎設施,是你沒有蓋的那些

這章從頭到尾在講「沒有蓋的東西」:沒有 message queue(掃表)、沒有 notification service(兩個欄位)、沒有站內通知(email)、沒有催付功能(一次查詢)。每個「沒有」都不是能力不足,是判斷——在需求被證明之前,基礎設施是負債不是資產。而支撐這些「沒有」的,是幾個做得特別紮實的「有」:可組合的查詢、事實粒度正確的表、現成的 Workspace。基礎設施的減法,靠的是地基的加法。

99% 的系統,1% 的人

綁定漏斗收到 99%,通知送達 99%,催付最後靠電話——這個系統到處都是同一個形狀:自動化把大宗收走,把殘量標記出來,交給人。很多工程師把「還有 1% 要人工」當成系統的恥辱,拚命想把它自動化到零;這個系統把 1% 當成設計的一部分:給它欄位、給它查詢、給它一個負責的角色。通知的終點不是「送出」,是「沒送到的,有人接」——把這句換成任何系統都成立:自動化的品質,不在覆蓋率的九有幾個,在殘量落地的姿勢好不好看。