歡迎來到第 21 天!昨天我們完成了後端的整理,讓路由本身變得更為單純,把服務的邏輯抽出管理,在結構上看起來遠比之前好看多了。我們昨天做的努力,除了本來就該做這樣的整理之外,另一方面則是為了後續的功能新增鋪路。過去 20 天,我們的應用程式是「無狀態」的,每個使用者進來,體驗都一樣。從今天起,我們要賦予它「記憶」。我們要建立一個真正的使用者系統,並打造一個專門的資料庫來儲存每一次的練習紀錄,讓我們的使用者之後可以透過練習紀錄來回顧自己的學習歷程,同時也讓我們的儀表板之後能有真正的資料套入。
要記住使用者,第一步就是要能識別他們。Supabase 提供了非常強大且易於整合的 Auth 服務,請按照以下的步驟操作:
ai-interviewer-knowledge-base
專案。![]() |
---|
圖1 :Authentication頁面 |
![]() |
---|
圖2 :啟用Email Provider |
完成這一步,你的 Supabase 後端就具備了處理使用者註冊、登入、登出的能力了。這就像我們蓋好了一家銀行的「開戶系統」,系統本身已經準備就緒,現有的平台服務在做 auth 的部分遠比以前簡化了許多,對於我們這樣的示範專案來說可說是再適合不過了。
補充說明:
你可能會想:「我們今天又不寫登入頁面,為什麼要先啟用 Authentication?」
這是一個很好的問題,答案在於技術相依性。我們即將建立的 practice_records
資料表,其中有一個 user_id
欄位,它需要參考 (REFERENCES) Supabase 內建的 auth.users
表。而 auth.users
這張儲存所有使用者資訊的特殊資料表,只有在你第一次啟用 Authentication 功能後,Supabase 才會自動幫你建立。
所以,我們的執行順序必須是:
啟用 Auth -> 讓 auth.users
表被建立出來。
建立 practice_records
表 -> 才能成功地將 user_id
關聯到 auth.users
。
practice_records
資料表這是我們今天最核心的任務。這張表將是我們所有使用者數據的基石,就像是銀行的「金庫」。一個好的資料庫設計,能讓未來的開發事半功倍。
我們需要的欄位如下:
欄位名稱 (name) | 型別 (type) | 說明 |
---|---|---|
id | uuid | 主鍵 (Primary Key)。使用 uuid_generate_v4() 自動生成。 |
created_at | timestamptz | 紀錄的建立時間。使用 now() 作為預設值。 |
user_id | uuid | 【核心關聯】外鍵,關聯到 auth.users 表的 id。用來標示這筆紀錄是誰的。 |
question_id | text | 練習的題目 ID,對應到我們 questions.json 中的 id。 |
user_answer | text | 使用者提交的完整答案(文字或程式碼)。 |
evaluation | jsonb | AI 回傳的完整評估 JSON 物件。使用 jsonb 型別可以讓我們未來對 JSON 內容進行高效查詢。 |
score | int2 | 從 evaluation 中提取出的分數 (1-5)。雖然 evaluation 裡有,但獨立出來方便未來做統計與排序。 |
如果你對於後端資料表比較沒什麼接觸的話,那你可能會好奇「使用者和練習紀錄是如何對應的?」,我這邊簡單說明一下,這個問題的核心就在於 user_id
這個欄位。整個流程如下:
程式碼片段
graph TD
A[使用者A 註冊/登入] --> B{Supabase Auth}
B --> C[在 auth.users 表中<br/>建立一筆使用者紀錄<br/>得到獨一無二的 User_A_ID]
D[使用者A 提交答案] --> E{我們的 /api/interview/evaluate API}
E --> F[1.取得當前登入者的 ID<br/>也就是 User_A_ID]
F --> G[2.將練習結果連同<br/>User_A_ID 一起寫入]
G --> H[在 practice_records 表中<br/>建立一筆新紀錄]
C -.-> I[practice_records.user_id]
H -.-> I
style I fill:#f9f,stroke:#333,stroke-width:2px
簡單來說,每個使用者註冊時都會獲得一個獨一無二的 user_id
。當我們儲存練習紀錄時,會把這個 user_id
一起存進去,就像在每筆紀錄上蓋上主人的印章,這樣就能清楚地將使用者和他們的練習紀錄完美地對應起來。
設計好了,我們就用 Supabase 的 SQL Editor 來建立它。
-- 建立 practice_records 資料表
CREATE TABLE public.practice_records (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
question_id TEXT NOT NULL,
user_answer TEXT,
evaluation JSONB,
score SMALLINT CHECK (score >= 1 AND score <= 5)
);
-- 建立索引以提升查詢效能
CREATE INDEX idx_practice_records_user_id ON public.practice_records(user_id);
CREATE INDEX idx_practice_records_created_at ON public.practice_records(created_at DESC);
CREATE INDEX idx_practice_records_question_id ON public.practice_records(question_id);
-- 啟用 RLS
ALTER TABLE public.practice_records ENABLE ROW LEVEL SECURITY;
這段 SQL 腳本主要執行了三大任務:建立結構、優化效能、以及設定安全基礎。
practice_records
主資料表:REFERENCES auth.users(id) ON DELETE CASCADE
:確保每筆紀錄都必須對應到一個真實的使用者,並且當使用者帳號被刪除時,其所有相關的練習紀錄也會自動一併清除。CHECK (score >= 1 AND score <= 5)
:在資料庫層級強制要求 score
欄位的值只能在 1 到 5 之間,從源頭保證了資料的正確性。user_id
、created_at
(降序)、和 question_id
這三個未來最可能被用來篩選或排序的欄位上建立了索引。ENABLE ROW LEVEL SECURITY
則是整個使用者資料安全的核心。它像一個總開關,預設會攔截所有讀取請求,為我們下一步撰寫「使用者只能讀取自己資料」的安全規則鋪平了道路。補充說明:什麼是 RLS ?
Row Level Security (RLS),中文常譯為「資料列級安全」或「行級安全」,是 PostgreSQL 資料庫一個極其強大的安全功能,也是 Supabase 安全模型的基石。
傳統的資料庫權限管理通常是針對「整張表」的,例如「Danny 這個使用者可以讀取 practice_records 這張表」。但這會帶來一個問題:Danny 可以讀取表中所有人的練習紀錄,這顯然是我們不想要的。
RLS 則將權限管理細化到了「每一列資料」,確保即便前端出了什麼差錯,其他使用者也無法讀取資料庫中的他人資料。
執行成功後,你就可以在「Table Editor」中看到這張全新的、空無一物的 practice_records
表了!
![]() |
---|
圖3 :新增的空資料表 |
今天我們為專案的「產品化」邁出了巨大的一步,從一個無狀態的工具,變成了一個具備使用者系統基礎的平台。
✅ 我們成功在 Supabase 中啟用了 Authentication 功能。
✅ 我們設計並建立了一張結構良好的 practice_records
資料表。
✅ 我們為新資料表開啟了 RLS,從第一天就貫徹了安全優先的原則。
重要:今天我們完成的是所有後端基礎建設。使用者還不能真的註冊,練習紀錄也還不會被儲存。這需要我們在接下來兩天完成前後端的串接工作。
資料庫的地基已經打好,門鎖也已裝上。明天 (Day 22),我們就要來打造真正的「大門」和「玄關」了。我們將回到前端,建立登入和註冊頁面,並整合 Supabase 的客戶端 Auth SDK,讓使用者能夠真正地登入我們的應用程式,並為讀取他們專屬的儀表板數據做好準備!