Coffee Shooters 咖啡槍手

31 篇文章憑空消失:一個寫死的 .limit(80),沒有任何 error

我沒刪過任何一篇文章,但有 31 篇從網站上徹底不見了——系列卡找不到、分類按鈕點不到、站內搜尋也搜不到。沒有 error、沒有 log、不留痕跡。查了半天,兇手是一行 .limit(80)

症狀:資料還在,人卻不見了

我的咖啡品牌網站有一個文章區,累積了好幾年的內容,從 2020 年最早期的咖啡筆記到最近的開發雜記都有。

某天我在整理內容時發現不對勁:最早期那批咖啡文章,好像從前台消失了。系列卡的計數少了、分類按鈕點進去看不到、站內搜尋打關鍵字也搜不出來。

但我很確定我沒刪過它們。我去資料庫查,文章好端端躺在那裡,status 是 published,什麼都正常。

資料還在,前台卻看不到——這種 bug 比「資料真的被刪了」更讓人發毛,因為你完全不知道從哪查起。沒有錯誤訊息可以搜,沒有堆疊可以追。它不是壞掉,是「安靜地少了一塊」。

根因:.limit(80) 的靜默截斷

我去翻文章列表頁 ContentList.vue 的資料查詢,看到這行:

.limit(80)

這是很久以前寫的,當時文章大概只有五六十篇,80 綽綽有餘。我實測了一下當下真實的已發布文章數:111 篇

問題就在這。前端一次只撈 80 篇,超過的 31 篇——多數正好是 2020 到 2023 年那批最早期的咖啡文章——根本沒被撈進來。而它們不是「撈進來但沒顯示」,是從一開始就不在前端的資料裡。所以系列卡的計數、分類的分桶、站內的搜尋,全部都是在這 80 篇的範圍內運作,那 31 篇對整個前台來說等於不存在。

.limit(80) 最坑的地方在於它不會報錯。它不是「資料太多、放棄、拋例外」,而是「安靜地給你前 80 筆,剩下的當作沒有」。這就是靜默截斷(silent truncation)——最危險的那種 bug,因為系統的每個部分看起來都運作正常,只是餵給它們的資料本身就少了一塊。

止血:先花 30 秒把 80 改 200

發現根因之後,我沒有馬上做「正確」的大改。我先做了止血:

.limit(200)

一行、30 秒、31 篇文章立刻回來。

我知道 200 不是根治——今天是 111 篇,哪天寫到 201 篇,同樣的 bug 會一模一樣地再發生。但止血和根治是兩件事:31 篇文章正在對真實訪客消失,這個當下最重要的是先讓它們回來,而不是花一小時做完美的架構再上線。

權宜擴容不是罪。罪是「改完 200 就忘了」,讓它變成下一個定時炸彈。所以我在改的同時,就把根治排進了待辦——而且當天就做了。

根治:把上限這件事從前端拿掉

真正的解法是讓「一次全載」這個前提消失。我做了三件事:

1. 後端分批 RPC。 新增一個 fn_get_content_list_page,接受 limit / offset,前端改成「先載第一批 30 篇,使用者往下滑再載更多」。這樣不管總數是 111 還是 1111,前端永遠不會一次撈爆。這個 RPC 順便把全站關鍵字搜尋也做進去(包含比對舊文章的英文 slug 命名),還把「閱讀分鐘數」在資料庫端算好回傳,前端不用自己算。

2. 計數跟列表分開。 原本的計數是 pages.value.filter(...).length——用「已經載進前端的文章」去算「這個分類有幾篇」。這是雙重錯誤:它同時把「載入量」和「計數正確性」綁死在同一個上限上。只要載入被截斷,計數就跟著錯。我改成獨立的 countRows 查詢,直接問資料庫「這個 category、這個 series_slug 有幾篇」,只查兩個窄欄位,不搬整包內容。「有幾篇」和「載哪幾篇」本來就是兩個問題,不該共用一個資料來源。

3. 列表不搬整包內容。 原本列表查詢會把每篇的 content jsonb(完整文章內容)整包搬過來,光是列表頁就扛著所有文章的全文。我讓列表只回摘要、封面圖、閱讀分鐘這些扁平欄位。單批 payload 從 348954 bytes 降到 22114 bytes,大概少了 94%。

驗證:「不見的回來了」要用數字證明

修「消失的資料」有個陷阱:你很容易改完看一眼「喔文章回來了」就收工。但我要的是證明數字精確吻合,不是「看起來變多了」。

我用兩條線交叉驗證:

修之前,分類按鈕加起來是 8+51+6 = 65,跟資料庫對不上(缺 15)。修之後變成 23+70+13 = 106,加上 5 篇已知的例外情況(跨分類的特殊文章),正好 111,跟資料庫的總數吻合到個位數。Playwright 也確認了「先載 30、載入更多再疊 60」沒有重複、沒有漏。

數字對上,我才敢說修好了。

給你帶走的三件事

  1. 任何前端 .limit(N) 撈列表,都要先問「N 之後會怎樣」。 多數情況是靜默截斷——不報錯,只是安靜地少給你資料。這是最難查的一類 bug,因為系統每個部分看起來都正常。
  1. 「有幾篇」跟「載哪幾篇」是兩個問題。 別用「載進來的陣列長度」當計數,它會隨著載入上限一起錯。計數就去問資料庫要 COUNT,列表就分批載,兩者分開。
  1. 修完消失的資料,用資料庫層的 COUNT 逐桶比對前台顯示。 數字精確吻合才算修好,「看起來變多了」不算。可以的話用 Playwright 真的去點、去滑,模擬真實使用者的載入路徑。

那行 .limit(80) 在我的 code 裡躺了好幾年,一直到文章數悄悄越過 80 才發作。它提醒我一件事:寫死的數字不會通知你它過期了,它只會在你沒注意的時候,安靜地開始說謊。