ORM 在騙你:當 populate / include 悄悄失效

症狀:資料明明在,前端就是拿不到 一個 CMS 系統的文件列印預覽,每份文件 header 都固定顯示「上傳者:—」。DB 裡明明有資料,API 回 HTTP 200 OK,回應裡的其他欄位(日期、文件類型、附件)都對——唯獨 uploadedBy 是 null。 沒錯誤、沒警告,伺服器 log 也乾淨。前端碰到 uploadedBy?.realName || '-' 就乖乖畫 dash。使用者以為這份文件沒指派擁有者,作者本人以為畫面壞了。大家都錯,但系統看起來好好的。 這是最難纏的 bug——靜默失敗。 先看 ORM 層在整個架構哪裡 要理解為什麼這類 bug 發生、也為什麼解法是「繞過 ORM」,先把架構分層看清楚: 為什麼 bug 只會發生在 ORM 層? 上面的業務邏輯只負責說「給我文件,順便帶上傳者」——它不知道要怎麼 JOIN。下面的 DB 只回應被問到的 SQL——你沒 SELECT 的欄位它不會主動給你。只有 ORM 層有權力決定「要翻譯成什麼 SQL」,也只有它遇到不會處理的組合時,會選擇「悄悄回你 null」而不是「拋錯給你看」。 這就是為什麼靜默失敗只會出現在這一層。 用 curl 逼它露餡 把 query 簡化到只剩 filter + populate 兩件事,一個一個比對: # A: 用 id filter → 關聯欄位變 null ❌ curl "$URL?filters[owner][id][\$eq]=3807&populate[uploadedBy]=true" # { "uploadedBy": null } # B: 用 documentId filter → 關聯正常回來 ✅ curl "$URL?filters[owner][documentId][\$eq]=abc123&populate[uploadedBy]=true" # { "uploadedBy": { "id": 85, "realName": "J. Chen" } } 同樣的 populate、同樣的文件集合,差別只在「怎麼辨認 owner」。這種事應該是 ORM 內部細節,對使用者不該有差。但就是有。 ...

April 24, 2026 · 2 分鐘 · Peter

Strapi v5's Silent populate Failure with Relation Filters

Symptom: A Relation Everyone Believes Is Empty A document management print preview kept rendering “Uploader: —” for every report. The database had the data. The API returned HTTP 200 OK. Yet the uploadedBy relation came back as null, with no error or warning anywhere. Users assumed reports had no uploader assigned. Authors assumed the UI was broken. Nobody suspected the API, because it was perfectly silent. Where the Bug Lives in the Stack Before the curl session, it helps to fix where the failure happens: ...

April 24, 2026 · 5 分鐘 · Peter

頭貼切不回去?一個 Bug 揪出 Admin Panel 權限漂移的跨後端通病

症狀:選完手機照片,畫面卻還是貼圖 使用者回報的步驟很簡單:點頭像 → 選「從手機圖庫」→ 選張照片 → 儲存。跳回會員中心,頭像還是原本的系統貼圖。 再點進去編輯頁,看到的又是手機照片。明明存成功了,為什麼 UI 顯示不一致? 這個 bug 表面上是前端畫面問題,但用 log 逼出來的真相,藏在三層之外——包括一個會在幾乎所有 admin-panel 型後端都出現的經典陷阱。 偵錯:跟著 log 走,別信錯誤訊息 第一步是打開前端 log 重現流程。關鍵幾行: StickerService -> Deactivating all stickers StickerService -> Error: status code of 403 StickerService -> Error Response: {status: 403, message: Forbidden} StickerService -> Deactivate all failed: You do not own this sticker 最後這句「You do not own this sticker」幾乎讓我停在錯誤方向——去查使用者到底擁有哪幾個貼圖。但翻前端程式碼才發現:那不是後端回的訊息,是前端 _handleDioError 把「403 Forbidden」硬翻譯成這句。 **這是第一個教訓:HTTP 狀態碼與文字訊息之間的語意對應是會說謊的。**真正的錯誤只有一個字——Forbidden。跟 ownership 無關,純粹是權限問題。 第一層根因:Admin Panel 的權限設定漂移 翻 Strapi admin → Settings → Users & Permissions → Roles → Authenticated → Sticker section,發現 deactivateAll 這支 route 的權限沒勾。 ...

April 17, 2026 · 2 分鐘 · Peter

Strapi Plugin 覆寫陷阱:為什麼你的 Override 沒有生效?

前言:一個看似簡單的需求 需求很單純:讓 Email 驗證信中的連結顯示前端網址(www.example.com),而不是後台網址(api.example.com)。這樣用戶不會看到內部系統架構。 外包商寫了這段程式碼: // ❌ 看起來合理,但完全沒效果 plugin.services.user.sendConfirmationEmail = async function(user) { // 自訂邏輯... } 部署後,Email 中的連結依然指向後台。為什麼? 問題根源:Factory Function 陷阱 翻開 Strapi 原始碼,發現 plugin.services.user 不是一個物件,而是一個 factory function: // Strapi 內部實作(簡化版) plugin.services.user = (context) => { return { sendConfirmationEmail: async (user) => { /* 原始邏輯 */ }, // 其他方法... } } 這意味著什麼? 每次 Strapi 需要 user service 時,它會呼叫這個 factory function 來取得一個新的 service 實例。你直接覆蓋 sendConfirmationEmail 屬性,等於在一個 function 物件上加屬性——factory 被呼叫時根本不會讀取這個屬性。 不這樣理解會怎樣? 你會像外包商一樣,花兩天 debug 卻找不到原因,因為程式碼完全沒有報錯,只是靜靜地被忽略。 解決方案:Factory Wrapper 模式 正確的做法是包裝原本的 factory function: // ✅ Factory Wrapper 模式 const originalUserServiceFactory = plugin.services.user; plugin.services.user = (context) => { // 先取得原始 service 實例 const originalUserService = originalUserServiceFactory(context); return { ...originalUserService, // 保留所有原始方法 // 覆寫特定方法 async sendConfirmationEmail(user) { // 你的自訂邏輯 const confirmationUrl = `${FRONTEND_URL}/verifyEmail`; // ... }, }; }; 為什麼這樣有效? ...

January 15, 2026 · 2 分鐘 · Peter

為什麼技術選型 CMS 我要選 Strapi?2024 年中的預算與系統分析決策

引言:一個技術選型的起點 2024 年 6 月,我坐在會議室裡,面對著老闆和行銷總監,準備報告我對公司新系統 CMS 的技術選型建議。這不是一個輕鬆的決定——選錯了,可能浪費數百萬的開發成本;選對了,能為公司省下可觀的人力支出。 經過數週的研究與分析,作為一個架構規劃師同時也是技術決策者,我最終選擇了 Strapi 作為我們的 Headless CMS 解決方案。這篇文章將分享我的決策過程、考量因素,以及最重要的——這個選擇如何為公司省下大筆預算。 什麼是 Headless CMS? 在深入 Strapi 之前,先理解 Headless CMS 的核心概念。 傳統 CMS vs Headless CMS 傳統 CMS 將前端與後端緊密耦合,網站的外觀和內容管理綁在一起。而 Headless CMS 則專注於內容管理和 API 提供,讓前端團隊可以使用任何技術框架來消費這些 API。 Headless CMS 的核心優勢 特性 傳統 CMS Headless CMS 前後端耦合 緊密耦合 完全分離 前端技術選擇 受限於 CMS 模板 任意框架(Vue、React、Flutter) 多平台支援 僅限網頁 Web、App、IoT 皆可 擴展性 中等 極高 API 優先 否 是 為什麼選擇 Strapi?三大核心原因 原因一:前後端完全分離 Strapi 作為 Headless CMS,天生就是為了前後端分離而設計。這意味著: ...

December 23, 2025 · 3 分鐘 · Peter

前端登入失敗的真兇:深入理解 CORS 問題與實戰解法

前言:那個令人抓狂的錯誤訊息 在開發前後端分離的 Web 應用時,幾乎每位工程師都曾遇過這個令人頭痛的錯誤: Access to fetch at 'http://localhost:1337/api/auth/local' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 這個錯誤通常發生在最關鍵的時刻: 前端登入功能即將完成,卻無法呼叫後端 API 串接第三方服務時,資料無法正常取得 部署到測試環境後,原本運作正常的功能突然失效 CORS(Cross-Origin Resource Sharing,跨來源資源共用)是現代 Web 開發中的核心安全機制,但也是許多開發者的痛點。這篇文章將從基礎原理到實戰應用,帶你完整理解 CORS 的運作方式,並提供實際可用的解決方案。 為什麼需要 CORS?從同源政策說起 同源政策的誕生 在理解 CORS 之前,我們需要先認識「同源政策」(Same-Origin Policy, SOP)。這是瀏覽器最基本的安全機制,在 1995 年 Netscape Navigator 2.0 引入 JavaScript 時就已經存在。 同源政策的目的:防止惡意網站讀取另一個網站的敏感資料。 想像一個情境:你登入了網路銀行(https://bank.com),此時你的瀏覽器保存了銀行的登入 Cookie。如果沒有同源政策,當你不小心訪問了一個惡意網站(https://evil.com),該網站的 JavaScript 就能透過你的瀏覽器向 https://bank.com 發送請求,並讀取你的帳戶資料。 同源政策阻止了這種攻擊:https://evil.com 的 JavaScript 無法讀取 https://bank.com 的回應內容。 什麼是「同源」? 兩個 URL 被視為同源,必須滿足以下三個條件: ...

July 1, 2025 · 9 分鐘 · Peter

Strapi 自訂搜尋功能:讓 Admin 面板支援多欄位搜尋

前言 在開發 Strapi 後台管理系統時,我遇到一個實際的使用者體驗問題:在「已購買課程」列表中,管理員可以用「真實姓名」搜尋到用戶的購課記錄,但用「電話號碼」卻搜尋不到。這個不一致的行為讓管理員感到困惑。 本文將分享如何在 Strapi v5 中實作自訂搜尋邏輯,讓 Admin 面板支援跨關聯的多欄位搜尋。 問題場景 資料結構: purchased-course(已購買課程):關聯到 user user(用戶):包含 email、realName、phoneNumber、username、nickname 等欄位 問題現象: ✅ 搜尋「真實姓名」→ 可以找到對應的購課記錄 ❌ 搜尋「電話號碼」→ 找不到任何結果 為什麼會這樣? 因為 Strapi 預設的搜尋功能只會搜尋當前 Collection 的直接欄位,不會自動搜尋關聯表(relation)的欄位。當我們在 purchased-course 列表搜尋時,Strapi 只會在 purchased-course 本身的欄位中搜尋,而不會去搜尋關聯的 user 資料。 解決方案架構 Strapi 提供了三個層級可以自訂搜尋邏輯: 1. Controller 層(API 端點) 位置: src/api/purchased-course/controllers/purchased-course.ts 用途: 處理前端透過 REST API 的搜尋請求 2. Service 層(業務邏輯) 位置: src/api/purchased-course/services/purchased-course.ts 用途: 封裝可重複使用的業務邏輯 3. Content Manager Plugin Extension(Admin 面板) 位置: src/extensions/content-manager/strapi-server.ts 用途: 最重要! 這是讓 Admin 面板搜尋生效的關鍵 ...

October 27, 2024 · 5 分鐘 · Peter