昨天,我們終於把廣播的 UI 做出來了。
Table 有了,Card 有了,確認按鈕能按,回覆也能送。
除了這些以外,今天我還讓 Agent 把 Backend API 補齊,順便把 Angular 跟 FastAPI 接了起來。
至此,Mock Data 終於可以下班了。
恭喜恭喜。
所以今天要寫什麼?
今天要來解決一個問題。
如果老師在另一台電腦發布廣播,學生的畫面要怎麼知道?
重新整理啊。
……
怎樣?有問題嗎?
沒有。
但如果我們希望廣播能自動更新,總不能要求資訊股長每隔幾秒就按一次 F5 吧?
那就每隔幾秒自動重新整理啊。
你還真是個天才。
既然要自動更新,最直覺的方法就是 Polling(輪詢)。
例如每隔 30 秒,向 Backend 發送一次 Request,取得當週所有廣播。
Client Server
| |
|---- GET /broadcasts -->|
|<--- 當週全部廣播 -------|
| |
| 30 秒後 |
| |
|---- GET /broadcasts -->|
|<--- 當週全部廣播 -------|
簡單、直接,而且確實能解決問題。
但這樣有個缺點。
即使這 30 秒內什麼事情都沒發生,我們還是得把整週的廣播重新傳送一次。
可是廣播又沒有很多。
確實。
以我們學校目前的使用情境來說,這個傳輸量可能還不到需要擔心的程度。
但既然都重新設計這套系統了,我還是想試試看能不能做得更精緻一點。
畢竟,我們真正想知道的不是「現在有什麼廣播」。
而是:
「我手上的資料,跟你手上的資料,有哪裡不一樣?」
Hash(雜湊)可以把一份資料轉換成固定長度的摘要。
這次我們使用 SHA-256。
假設 Client 手上有五則廣播,Server 也有相同的五則廣播。
只要雙方使用相同的資料表示方式,就能各自計算出相同的 Hash。
Client Server
五則廣播 五則廣播
↓ ↓
SHA-256 SHA-256
↓ ↓
abc123... abc123...
| |
|-------- 比對 Hash -------->|
|<------- 沒有變更 ----------|
如果資料不同,Hash 通常也會不同。
通常?
對,Hash 並不是數學上保證不同資料一定產生不同結果。
畢竟輸入資料的可能性遠遠多於 SHA-256 能表示的摘要數量,理論上一定存在碰撞。
但對我們這個用途而言,SHA-256 的碰撞風險已經足夠低。
所以,我們可以讓 Client 每隔 30 秒計算自己目前持有的資料,再把 Hash 送給 Server。
如果 Hash 相同,就不需要重新傳送資料。
如果不同,再取得需要更新的內容。
那這樣是不是只需要一個 Hash?
理論上可以。
但我不想每次只有一則廣播改變,就重新下載整週的資料。
所以,我們還需要一個東西。
我們把每五則廣播分成一個 Window。
例如這週有 12 則廣播:
Week 2026-W41
│
├── Window A
│ ├── Broadcast 01
│ ├── Broadcast 02
│ ├── Broadcast 03
│ ├── Broadcast 04
│ └── Broadcast 05
│
├── Window B
│ ├── Broadcast 06
│ ├── Broadcast 07
│ ├── Broadcast 08
│ ├── Broadcast 09
│ └── Broadcast 10
│
└── Window C
├── Broadcast 11
└── Broadcast 12
這樣一來,如果 Broadcast 07 發生變更,我們只需要處理 Window B。
等等。
如果老師又發布一則廣播呢?
好問題。
假設我們直接把廣播按照時間由新到舊排列,每五則切成一組。
只要新增一則廣播,後面所有廣播的位置就可能跟著改變。
原本在 Window A 的第五則廣播,可能被擠到 Window B。
Window B 的第五則又被擠到 Window C。
結果明明只新增了一則廣播,卻讓一大堆 Window 的 Hash 全部改變。
那不就白忙一場?
所以這次不能直接用陣列位置分組。
我們讓 Server 為 Window 分配固定的 UUID,並將廣播與 Window 的關係保存到資料庫。
每個 Window 最多五則,滿了就建立下一個。
而且每週獨立分組,不會把上週的廣播混進本週。
這次新增了兩張資料表:
broadcast_windows
broadcast_window_members
為了避免同一週有多個 Transaction 同時建立窗口,Agent 也加入了以週次為單位的交易鎖。
這樣就不會因為兩位老師同時發布廣播,而讓窗口分配出現競爭問題。
前面我們一直在講「廣播改變」。
但仔細想想,廣播發布之後,我們不是已經規定不能修改了嗎?
對啊。
那到底還有什麼東西會改變?
確認紀錄。
還有回覆。
假設 Window A 有五則廣播,其中一則新增了一筆回覆。
如果我們把廣播、確認、回覆全部算成同一個 Hash,那麼只要多一筆回覆,整個 Window 就會被判定為不同步。
最後還是得重新傳送一堆根本沒有改變的資料。
所以,我們把每個 Window 再拆成三層。
Window A
│
├── Broadcast Hash
│
├── Confirmation Hash
│
└── Reply Hash
這樣就能分別判斷:
例如資訊股長剛剛確認了一則廣播。
這時 Broadcast Hash 不變,Reply Hash 也不變。
只有 Confirmation Hash 會改變。
Server 就只需要把對應窗口的 Confirmation Layer 傳回 Client。
所以我們不只把廣播切成五則一組,還把每組切成三層?
沒錯。
這樣就能把需要重新傳送的資料範圍縮小。
當然,這不代表 Server 完全不需要查詢資料,也不代表每次輪詢都不消耗資源。
只是我們不用再把沒有改變的資料全部送回去了。
這裡還有一個問題。
Client 使用 TypeScript,Backend 使用 Python。
即使兩邊拿到的是同一份資料,也不代表直接序列化之後,得到的位元組一定相同。
例如欄位順序、時間格式、UUID 大小寫,甚至小數秒的表示方式,都可能影響最後的 Hash。
{"id": 1, "content": "Hello"}
跟:
{"content": "Hello", "id": 1}
雖然表達的資料相同,但直接拿這兩段 JSON 計算 SHA-256,結果並不相同。
所以要讓 Python 跟 TypeScript 用一樣的規則?
對。
這次我們訂出一份固定的 Hash Contract。
包含固定欄位陣列、穩定排序、UTF-8 編碼、小寫 UUID,以及統一使用 UTC、六位小數秒的時間表示方式。
Client 使用 Web Crypto API 真正計算 SHA-256,而不是單純保存 Server 上次提供的版本號。
另外也建立了 Python 與 TypeScript 共用的 Golden Test Vectors。
也就是先準備好相同的輸入資料與預期 Hash,再確認兩邊算出來的結果完全一致。
不然哪天 Backend 覺得資料沒變,Frontend 卻覺得整個世界都變了,那就有點尷尬了。
還有一個容易忽略的情況。
假設 Server 正在計算某個 Window 的 Hash。
算完之後,剛好有人新增一則回覆。
接著 Server 才開始讀取資料、準備回傳給 Client。
這時候,剛剛計算的 Hash 與實際回傳的資料,就可能不是同一個版本。
那怎麼辦?
這次 Agent 在同步比對與資料投影時,使用 PostgreSQL 的 Repeatable Read 隔離層級。
讓同一個 Transaction 內的查詢看到一致的資料快照。
至於資料量較大的 Layer,則使用分塊傳輸。
如果分頁期間偵測到快照版本已經改變,Server 會回傳 HTTP 409,Client 捨棄尚未完成的暫存資料,再重新同步。
需要注意的是,這不代表跨越多個 HTTP Request 的同步過程,都能共用同一個資料庫快照。
這次採用的方式,是透過版本檢查與後續輪詢逐步收斂到一致狀態。
除此之外,我們也建立了 broadcast_change_log。
它會與廣播、確認或回覆的新增操作在同一個 Transaction 中寫入,提供差異 Layer 的異動資訊。
但最終的一致性判斷仍然以 Hash 為準,而不是假設 Change Log 的自動遞增 ID 就代表 Transaction 的提交順序。
畢竟 PostgreSQL 的 Sequence 並不保證 Transaction 依照取得 ID 的順序 Commit。
……你不是只想做個每 30 秒更新一次的功能嗎?
對啊。
怎麼突然開始研究資料庫交易隔離了?
……
我也想知道。
總之,規格都訂好了,接下來就交給 Agent 實作。
這次保留原本的 Broadcast API 與 Angular UI,只新增同步機制。
最後完成了:
POST /broadcasts/sync 同步 API。另外,使用者自己按下確認或送出回覆時,畫面會立即更新,不需要傻傻等下一個 30 秒。
而在背景輪詢時,也不會因為資料同步,就把已經展開的回覆收起來,或清空正在輸入的草稿。
至於驗證結果:
| 項目 | 結果 |
|---|---|
| Backend 測試 | 97 passed |
| Frontend 測試 | 42 passed |
| Production / Mock Build | 成功 |
| Migration 升降版與回填 | 通過 |
| Chrome 多 Session 驗收 | 通過 |
Chrome 驗收使用真實 Session 與測試 PostgreSQL,並將輪詢間隔暫時縮短為一秒,驗證新廣播、確認、回覆、班級隔離、斷線恢復,以及重新整理後的資料一致性。
正式設定仍然是每 30 秒輪詢一次。
不過,目前完成的是測試環境驗收,正式環境還沒有套用 Migration,也尚未部署新版。
所以現在還不能說整套系統已經正式上線。
回頭看看今天的目標。
其實一開始,我只是希望老師發布廣播後,學生不用自己重新整理,就能看到新訊息。
最簡單的方法,是每 30 秒重新取得一次全部廣播。
但最後我們選擇讓 Client 自己計算 Hash,再由 Server 判斷哪些 Window、哪些 Layer 需要更新。
這樣即使資料沒有變動,Client 仍然會定期發送 Request,但不需要每次都把整週廣播重新下載一遍。
更重要的是,我們不只是追蹤「發生過哪些變更」,而是直接檢查「目前兩邊持有的資料是否一致」。
這讓網路中斷後的恢復,以及 Client 本地資料不一致時的修復,都有了比較明確的處理方式。
所以今天算是做完了?
測試環境裡,算是。
那明天呢?
不知道。
不過,至少從今天開始,廣播系統終於不用靠資訊股長一直按 F5 來維持新鮮了。
畢竟前面都說過了。
廣播會臭酸。
既然如此,至少不要讓它因為沒人重新整理,就一直躺在畫面外面發臭吧。