從零到一:用 Supabase 建立茶語時光後端系統 - 開源全端解決方案


深度解析如何使用 Supabase 開源全端平台打造茶飲預約系統,從 PostgreSQL 資料庫設計到即時 API 開發,涵蓋身份驗證、即時同步、檔案存儲等功能。提供完整的開源解決方案,具備 Firebase 便利性但擁有更開放的架構與更低成本。
專案概述:深度解析如何使用 Supabase 開源後端平台打造茶飲預約系統,從 PostgreSQL 資料庫設計到即時功能實作,完整的現代化開發指南。
🎯 為什麼選擇 Supabase?
在完成 Bubble.io 和 Xano 的茶語時光後端實作後,我們將探索第三種方案:Supabase。作為開源的 Firebase 替代品,Supabase 結合了前兩者的優點,並提供了獨特的價值主張:
Supabase vs Bubble vs Xano 核心差異
三平台全方位比較分析
從技術架構到商業價值的完整對比評估
比較項目 | 🫧 Bubble.io | ⚡ Xano | 🚀 Supabase |
---|---|---|---|
資料庫類型 | 🫧簡化視覺資料庫 | ⚡真正的 PostgreSQL | 🚀原生 PostgreSQL + 即時訂閱 |
API 設計 | 🫧工作流程導向 | ⚡標準 RESTful | 🚀自動生成 REST + GraphQL |
即時功能 | 🫧需額外開發 | ⚡需額外整合 | 🚀原生 WebSocket 支援 |
學習曲線 | 🫧最低(視覺化) | ⚡中等(開發者友善) | 🚀中等(SQL 基礎) |
擴展性 | 🫧有限(~100 用戶) | ⚡良好(~10K 用戶) | 🚀優秀(100K+ 用戶) |
廠商鎖定風險 | 🫧高度鎖定 | ⚡中度鎖定 | 🚀零鎖定(開源) |
開發速度 | 🫧最快(3週) | ⚡快速(2週) | 🚀很快(1.5週) |
成本效益 | 🫧中等 | ⚡較高 | 🚀最佳 |
社群支援 | 🫧商業支援 | ⚡有限社群 | 🚀活躍開源社群 |
自部署能力 | 🫧不支援 | ⚡企業版支援 | 🚀完全支援 |
Supabase 的核心優勢
🔓 開源透明
- 完全開源,避免廠商鎖定
- 可自部署,完全掌控數據
- 活躍的社群貢獻與支援
⚡ 即時能力
- 原生 WebSocket 支援
- 即時資料庫訂閱
- 完美適合訂單狀態即時更新
🛡️ 企業級安全
- Row Level Security (RLS)
- 多種認證方式整合
- 細粒度權限控制
🚀 開發者體驗
- 真正的 PostgreSQL
- 自動生成 RESTful API
- 強大的 SQL 編輯器
- TypeScript 原生支援
實戰案例:為什麼企業選擇 Supabase
某台灣電商平台使用 Supabase 成果:
- 開發時間:1.5 週完成 MVP,比 Bubble 快 50%
- 成本效益:免費額度支援 50,000 MAU,付費方案比 Xano 便宜 40%
- 即時功能:原生支援即時庫存更新,Bubble/Xano 需額外開發
- 可擴展性:輕鬆支援 100,000+ 用戶,且可隨時自部署擴展
- 開源保障:避免平台關閉風險,永久掌控數據主權
🏗️ Supabase 專案建置完整指南
第一步:Supabase 專案初始化
1. 創建新專案
🚀 Supabase 專案建置步驟指南
從零開始,15 分鐘完成企業級後端設定
設定步驟
建立 Supabase 帳號
2 分鐘註冊免費帳號並驗證 Email
詳細步驟:
- 1前往 supabase.com 註冊帳號
- 2使用 GitHub/Google OAuth 快速註冊
- 3驗證電子郵件地址
- 4完成 onboarding 流程
# 或使用 CLI 註冊
npm install -g supabase
supabase login
💡 小提示: 建議按順序完成每個步驟,確保系統設定正確。遇到問題可參考 Supabase 官方文檔。
2. 專案配置
基本設定:
# Supabase 專案資訊
Project Name: chayu-time-booking
Organization: 你的組織名稱
Database Password: [自動生成強密碼]
Region: Southeast Asia (Singapore) # 台灣最近的區域
API 設定擷取:
// 在 Supabase Dashboard → Settings → API 取得
const supabaseConfig = {
url: 'https://your-project-ref.supabase.co',
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
serviceRoleKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' // 僅後端使用
};
3. 本地開發環境設置
Supabase CLI 安裝:
# 安裝 Supabase CLI
npm install -g supabase
# 初始化本地專案
supabase init
# 啟動本地開發環境
supabase start
# 連結到遠端專案
supabase link --project-ref your-project-ref
本地開發優勢:
{
"local_development": {
"database": "本地 PostgreSQL 實例",
"api": "本地 API 伺服器",
"auth": "本地認證服務",
"storage": "本地檔案儲存",
"edge_functions": "本地 Deno runtime"
},
"benefits": [
"離線開發能力",
"快速迭代測試",
"數據遷移驗證",
"完整的開發環境隔離"
]
}
🗄️ 第二步:PostgreSQL 資料庫設計
1. Users 用戶表 👤
SQL Schema 建立:
-- 在 Supabase SQL Editor 中執行
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
line_user_id TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
picture_url TEXT,
phone_number TEXT,
email TEXT,
membership_level TEXT DEFAULT 'bronze' CHECK (membership_level IN ('bronze', 'silver', 'gold')),
points_balance INTEGER DEFAULT 0 CHECK (points_balance >= 0),
wallet_balance DECIMAL(10,2) DEFAULT 0.00 CHECK (wallet_balance >= 0),
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL,
last_login TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW())
);
-- 建立索引優化查詢效能
CREATE INDEX idx_users_line_user_id ON users(line_user_id);
CREATE INDEX idx_users_membership_level ON users(membership_level);
CREATE INDEX idx_users_created_at ON users(created_at);
-- 建立更新時間觸發器
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = TIMEZONE('utc'::TEXT, NOW());
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_users_updated_at BEFORE UPDATE
ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- RLS (Row Level Security) 政策
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- 用戶只能讀取和更新自己的資料
CREATE POLICY "Users can view own data" ON users
FOR SELECT USING (auth.jwt() ->> 'line_user_id' = line_user_id);
CREATE POLICY "Users can update own data" ON users
FOR UPDATE USING (auth.jwt() ->> 'line_user_id' = line_user_id);
-- 允許註冊新用戶
CREATE POLICY "Enable insert for authenticated users" ON users
FOR INSERT WITH CHECK (true);
TypeScript 類型定義:
// types/database.ts
export interface User {
id: string;
line_user_id: string;
display_name: string;
picture_url?: string;
phone_number?: string;
email?: string;
membership_level: 'bronze' | 'silver' | 'gold';
points_balance: number;
wallet_balance: number;
created_at: string;
updated_at: string;
last_login: string;
}
2. Products 商品表 🍵
完整商品資料表:
CREATE TABLE products (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
price DECIMAL(10,2) NOT NULL CHECK (price > 0),
category TEXT NOT NULL,
description TEXT,
image_url TEXT,
rating DECIMAL(3,2) CHECK (rating >= 0 AND rating <= 5),
review_count INTEGER DEFAULT 0 CHECK (review_count >= 0),
preparation_time INTEGER DEFAULT 15 CHECK (preparation_time > 0),
stock_quantity INTEGER DEFAULT 100 CHECK (stock_quantity >= 0),
low_stock_threshold INTEGER DEFAULT 10 CHECK (low_stock_threshold >= 0),
availability_status BOOLEAN DEFAULT true,
tags TEXT[] DEFAULT '{}',
nutrition_info JSONB DEFAULT '{}',
customization_options JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL
);
-- 效能優化索引
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_availability ON products(availability_status);
CREATE INDEX idx_products_rating ON products(rating DESC);
CREATE INDEX idx_products_tags ON products USING GIN(tags);
CREATE INDEX idx_products_price ON products(price);
-- 全文搜尋索引
CREATE INDEX idx_products_search ON products
USING GIN(to_tsvector('english', name || ' ' || COALESCE(description, '')));
-- 更新時間觸發器
CREATE TRIGGER update_products_updated_at BEFORE UPDATE
ON products FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- RLS 政策 - 產品對所有人可見
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Products are viewable by everyone" ON products FOR SELECT USING (true);
CREATE POLICY "Only admins can modify products" ON products
FOR ALL USING (auth.jwt() ->> 'role' = 'admin');
商品客製化選項範例:
{
"sweetness": {
"type": "select",
"options": ["無糖", "微糖", "半糖", "少糖", "正常糖"],
"default": "半糖"
},
"ice_level": {
"type": "select",
"options": ["去冰", "微冰", "少冰", "正常冰"],
"default": "少冰"
},
"toppings": {
"type": "multi_select",
"options": [
{"name": "珍珠", "price": 10},
{"name": "椰果", "price": 8},
{"name": "布丁", "price": 15},
{"name": "紅豆", "price": 12}
],
"max_selections": 3
},
"size": {
"type": "select",
"options": [
{"name": "中杯", "price_modifier": 0},
{"name": "大杯", "price_modifier": 10}
],
"default": "中杯"
}
}
3. Stores 門市表 🏪
進階門市管理:
CREATE TABLE stores (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
address TEXT NOT NULL,
phone TEXT NOT NULL,
email TEXT,
operating_hours JSONB NOT NULL,
special_hours JSONB DEFAULT '{}', -- 特殊營業時間(節日等)
current_queue_count INTEGER DEFAULT 0 CHECK (current_queue_count >= 0),
average_wait_time INTEGER DEFAULT 10 CHECK (average_wait_time >= 0),
max_concurrent_orders INTEGER DEFAULT 50 CHECK (max_concurrent_orders > 0),
latitude DECIMAL(10,8),
longitude DECIMAL(11,8),
store_status TEXT DEFAULT 'open' CHECK (store_status IN ('open', 'busy', 'closed', 'maintenance')),
facilities TEXT[] DEFAULT '{}', -- 設施: WiFi, 停車場, 外送等
payment_methods TEXT[] DEFAULT '{"cash", "card", "linepay", "wallet"}',
manager_id UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL
);
-- 地理位置搜尋優化
CREATE INDEX idx_stores_location ON stores USING GIST(ll_to_earth(latitude, longitude));
CREATE INDEX idx_stores_status ON stores(store_status);
CREATE INDEX idx_stores_facilities ON stores USING GIN(facilities);
-- 地理位置查詢函數
CREATE OR REPLACE FUNCTION nearby_stores(
user_lat DECIMAL,
user_lng DECIMAL,
radius_km DECIMAL DEFAULT 5
)
RETURNS TABLE(
store_id UUID,
name TEXT,
distance_km DECIMAL
) AS $$
BEGIN
RETURN QUERY
SELECT
s.id,
s.name,
ROUND(earth_distance(ll_to_earth(user_lat, user_lng), ll_to_earth(s.latitude, s.longitude)) / 1000, 2) as distance_km
FROM stores s
WHERE s.store_status = 'open'
AND earth_distance(ll_to_earth(user_lat, user_lng), ll_to_earth(s.latitude, s.longitude)) <= radius_km * 1000
ORDER BY distance_km;
END;
$$ LANGUAGE plpgsql;
-- RLS 政策
ALTER TABLE stores ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Stores are viewable by everyone" ON stores FOR SELECT USING (true);
4. Orders 訂單表 📋
完整訂單系統:
CREATE TABLE orders (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
order_number TEXT UNIQUE NOT NULL, -- 人類可讀的訂單編號
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
store_id UUID NOT NULL REFERENCES stores(id) ON DELETE RESTRICT,
order_items JSONB NOT NULL,
total_amount DECIMAL(10,2) NOT NULL CHECK (total_amount > 0),
discount_amount DECIMAL(10,2) DEFAULT 0 CHECK (discount_amount >= 0),
final_amount DECIMAL(10,2) GENERATED ALWAYS AS (total_amount - discount_amount) STORED,
payment_method TEXT NOT NULL CHECK (payment_method IN ('wallet', 'points', 'linepay', 'credit', 'cash')),
payment_status TEXT DEFAULT 'pending' CHECK (payment_status IN ('pending', 'paid', 'failed', 'refunded')),
order_status TEXT DEFAULT 'pending' CHECK (order_status IN ('pending', 'confirmed', 'preparing', 'ready', 'completed', 'cancelled')),
order_type TEXT NOT NULL CHECK (order_type IN ('pickup_now', 'pickup_scheduled', 'delivery')),
scheduled_time TIMESTAMP WITH TIME ZONE,
estimated_pickup_time TIMESTAMP WITH TIME ZONE,
actual_pickup_time TIMESTAMP WITH TIME ZONE,
special_instructions TEXT,
loyalty_points_used INTEGER DEFAULT 0,
loyalty_points_earned INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL
);
-- 訂單編號生成函數
CREATE OR REPLACE FUNCTION generate_order_number()
RETURNS TEXT AS $$
DECLARE
new_number TEXT;
counter INTEGER;
BEGIN
-- 格式: CY20250615001 (CY + 日期 + 流水號)
SELECT COALESCE(MAX(CAST(RIGHT(order_number, 3) AS INTEGER)), 0) + 1
INTO counter
FROM orders
WHERE order_number LIKE 'CY' || TO_CHAR(NOW(), 'YYYYMMDD') || '%';
new_number := 'CY' || TO_CHAR(NOW(), 'YYYYMMDD') || LPAD(counter::TEXT, 3, '0');
RETURN new_number;
END;
$$ LANGUAGE plpgsql;
-- 自動生成訂單編號觸發器
CREATE OR REPLACE FUNCTION set_order_number()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.order_number IS NULL THEN
NEW.order_number := generate_order_number();
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_set_order_number
BEFORE INSERT ON orders
FOR EACH ROW EXECUTE FUNCTION set_order_number();
-- 效能索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_store_id ON orders(store_id);
CREATE INDEX idx_orders_status ON orders(order_status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_orders_order_number ON orders(order_number);
-- RLS 政策
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own orders" ON orders FOR SELECT USING (user_id = auth.uid());
CREATE POLICY "Users can insert own orders" ON orders FOR INSERT WITH CHECK (user_id = auth.uid());
5. Order Status History 訂單狀態歷史 📊
完整狀態追蹤:
CREATE TABLE order_status_history (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
previous_status TEXT,
new_status TEXT NOT NULL,
changed_by UUID REFERENCES users(id),
change_reason TEXT,
notes TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::TEXT, NOW()) NOT NULL
);
-- 自動記錄狀態變更
CREATE OR REPLACE FUNCTION log_order_status_change()
RETURNS TRIGGER AS $$
BEGIN
-- 只在狀態真正變更時記錄
IF OLD.order_status IS DISTINCT FROM NEW.order_status THEN
INSERT INTO order_status_history (
order_id,
previous_status,
new_status,
changed_by,
change_reason,
metadata
) VALUES (
NEW.id,
OLD.order_status,
NEW.order_status,
auth.uid(),
CASE
WHEN NEW.order_status = 'confirmed' THEN 'Order confirmed by system'
WHEN NEW.order_status = 'preparing' THEN 'Order started preparation'
WHEN NEW.order_status = 'ready' THEN 'Order ready for pickup'
WHEN NEW.order_status = 'completed' THEN 'Order completed'
WHEN NEW.order_status = 'cancelled' THEN 'Order cancelled'
ELSE 'Status updated'
END,
jsonb_build_object(
'timestamp', NOW(),
'previous_total', OLD.total_amount,
'new_total', NEW.total_amount
)
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_log_order_status_change
AFTER UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION log_order_status_change();
⚙️ 第三步:Supabase API 整合
1. JavaScript Client 設定
前端 Client 初始化:
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
},
realtime: {
params: {
eventsPerSecond: 10
}
}
});
// TypeScript 類型支援
export type Database = {
public: {
Tables: {
users: {
Row: User;
Insert: Omit<User, 'id' | 'created_at' | 'updated_at'>;
Update: Partial<Omit<User, 'id' | 'created_at'>>;
};
products: {
Row: Product;
Insert: Omit<Product, 'id' | 'created_at' | 'updated_at'>;
Update: Partial<Omit<Product, 'id' | 'created_at'>>;
};
orders: {
Row: Order;
Insert: Omit<Order, 'id' | 'order_number' | 'created_at' | 'updated_at'>;
Update: Partial<Omit<Order, 'id' | 'order_number' | 'created_at'>>;
};
};
};
};
2. 用戶管理功能 👤
LINE LIFF 整合註冊:
// services/userService.ts
import { supabase } from '@/lib/supabase';
export class UserService {
// 用戶註冊/登入
static async registerOrLoginUser(liffProfile: any) {
try {
// 檢查用戶是否已存在
const { data: existingUser, error: fetchError } = await supabase
.from('users')
.select('*')
.eq('line_user_id', liffProfile.userId)
.single();
if (fetchError && fetchError.code !== 'PGRST116') {
throw fetchError;
}
if (existingUser) {
// 更新最後登入時間
const { data: updatedUser, error: updateError } = await supabase
.from('users')
.update({
last_login: new Date().toISOString(),
display_name: liffProfile.displayName,
picture_url: liffProfile.pictureUrl
})
.eq('id', existingUser.id)
.select()
.single();
if (updateError) throw updateError;
return { user: updatedUser, action: 'login' };
} else {
// 創建新用戶
const { data: newUser, error: insertError } = await supabase
.from('users')
.insert({
line_user_id: liffProfile.userId,
display_name: liffProfile.displayName,
picture_url: liffProfile.pictureUrl,
membership_level: 'bronze',
points_balance: 0,
wallet_balance: 0
})
.select()
.single();
if (insertError) throw insertError;
return { user: newUser, action: 'register' };
}
} catch (error) {
console.error('用戶註冊/登入錯誤:', error);
throw error;
}
}
// 取得用戶資料
static async getUserData(lineUserId: string) {
const { data, error } = await supabase
.from('users')
.select('*')
.eq('line_user_id', lineUserId)
.single();
if (error) throw error;
return data;
}
// 更新用戶點數
static async updateUserPoints(userId: string, pointsChange: number) {
const { data, error } = await supabase
.rpc('update_user_points', {
user_id: userId,
points_change: pointsChange
});
if (error) throw error;
return data;
}
}
PostgreSQL 點數更新函數:
-- 建立安全的點數更新函數
CREATE OR REPLACE FUNCTION update_user_points(
user_id UUID,
points_change INTEGER
)
RETURNS users AS $$
DECLARE
updated_user users%ROWTYPE;
BEGIN
UPDATE users
SET
points_balance = GREATEST(0, points_balance + points_change),
updated_at = NOW()
WHERE id = user_id
RETURNING * INTO updated_user;
IF NOT FOUND THEN
RAISE EXCEPTION 'User not found with id: %', user_id;
END IF;
RETURN updated_user;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
3. 商品管理功能 🍵
進階商品查詢:
// services/productService.ts
export class ProductService {
// 取得商品列表(支援分類、搜尋、排序)
static async getProducts(options: {
category?: string;
search?: string;
sortBy?: 'name' | 'price' | 'rating';
sortOrder?: 'asc' | 'desc';
limit?: number;
offset?: number;
} = {}) {
let query = supabase
.from('products')
.select('*')
.eq('availability_status', true)
.gt('stock_quantity', 0);
// 分類篩選
if (options.category && options.category !== 'all') {
query = query.eq('category', options.category);
}
// 搜尋功能
if (options.search) {
query = query.textSearch('name,description', options.search);
}
// 排序
if (options.sortBy) {
query = query.order(options.sortBy, {
ascending: options.sortOrder === 'asc'
});
} else {
query = query.order('rating', { ascending: false })
.order('name', { ascending: true });
}
// 分頁
if (options.limit) {
query = query.limit(options.limit);
}
if (options.offset) {
query = query.range(options.offset, options.offset + (options.limit || 50) - 1);
}
const { data, error, count } = await query;
if (error) throw error;
return {
products: data,
totalCount: count,
hasMore: options.offset ? (options.offset + data.length) < (count || 0) : false
};
}
// 取得商品分類與統計
static async getProductCategories() {
const { data, error } = await supabase
.rpc('get_product_categories_stats');
if (error) throw error;
return data;
}
// 即時庫存訂閱
static subscribeToStockUpdates(productIds: string[], callback: (payload: any) => void) {
return supabase
.channel('stock-updates')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'products',
filter: `id=in.(${productIds.join(',')})`
},
callback
)
.subscribe();
}
}
商品分類統計函數:
-- 建立商品分類統計函數
CREATE OR REPLACE FUNCTION get_product_categories_stats()
RETURNS TABLE(
category TEXT,
product_count BIGINT,
avg_rating NUMERIC,
min_price NUMERIC,
max_price NUMERIC,
total_stock INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
p.category,
COUNT(*) as product_count,
ROUND(AVG(p.rating), 2) as avg_rating,
MIN(p.price) as min_price,
MAX(p.price) as max_price,
SUM(p.stock_quantity)::INTEGER as total_stock
FROM products p
WHERE p.availability_status = true
GROUP BY p.category
ORDER BY p.category;
END;
$$ LANGUAGE plpgsql;
4. 即時訂單管理 📋
完整訂單處理流程:
// services/orderService.ts
export class OrderService {
// 創建訂單
static async createOrder(orderData: {
userId: string;
storeId: string;
items: OrderItem[];
paymentMethod: string;
orderType: string;
scheduledTime?: string;
specialInstructions?: string;
}) {
try {
// 開始交易
const { data, error } = await supabase.rpc('create_order_transaction', {
p_user_id: orderData.userId,
p_store_id: orderData.storeId,
p_order_items: JSON.stringify(orderData.items),
p_payment_method: orderData.paymentMethod,
p_order_type: orderData.orderType,
p_scheduled_time: orderData.scheduledTime,
p_special_instructions: orderData.specialInstructions
});
if (error) throw error;
// 發送即時通知
await this.notifyOrderCreated(data.order_id);
return data;
} catch (error) {
console.error('訂單創建失敗:', error);
throw error;
}
}
// 訂單狀態即時訂閱
static subscribeToOrderUpdates(userId: string, callback: (payload: any) => void) {
return supabase
.channel(`user-orders-${userId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'orders',
filter: `user_id=eq.${userId}`
},
callback
)
.subscribe();
}
// 取得用戶訂單歷史
static async getUserOrders(userId: string, options: {
status?: string;
limit?: number;
offset?: number;
} = {}) {
let query = supabase
.from('orders')
.select(`
*,
stores:store_id (name, address, phone),
order_status_history (
previous_status,
new_status,
change_reason,
created_at
)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (options.status) {
query = query.eq('order_status', options.status);
}
if (options.limit) {
query = query.limit(options.limit);
}
if (options.offset) {
query = query.range(options.offset, options.offset + (options.limit || 20) - 1);
}
const { data, error } = await query;
if (error) throw error;
return data.map(order => ({
...order,
items: JSON.parse(order.order_items as string),
statusHistory: order.order_status_history
}));
}
// 即時通知
private static async notifyOrderCreated(orderId: string) {
// 發送到店家管理系統
const { error } = await supabase
.channel('store-notifications')
.send({
type: 'broadcast',
event: 'new-order',
payload: { orderId, timestamp: new Date().toISOString() }
});
if (error) console.error('通知發送失敗:', error);
}
}
複雜訂單交易函數:
-- 建立完整的訂單創建交易
CREATE OR REPLACE FUNCTION create_order_transaction(
p_user_id UUID,
p_store_id UUID,
p_order_items JSONB,
p_payment_method TEXT,
p_order_type TEXT,
p_scheduled_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_special_instructions TEXT DEFAULT NULL
)
RETURNS JSONB AS $$
DECLARE
v_order_id UUID;
v_total_amount DECIMAL(10,2) := 0;
v_user_record users%ROWTYPE;
v_store_record stores%ROWTYPE;
v_item JSONB;
v_product_record products%ROWTYPE;
v_points_earned INTEGER := 0;
v_estimated_pickup TIMESTAMP WITH TIME ZONE;
BEGIN
-- 驗證用戶
SELECT * INTO v_user_record FROM users WHERE id = p_user_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'User not found';
END IF;
-- 驗證門市
SELECT * INTO v_store_record FROM stores WHERE id = p_store_id AND store_status = 'open';
IF NOT FOUND THEN
RAISE EXCEPTION 'Store not available';
END IF;
-- 計算總金額並驗證商品
FOR v_item IN SELECT * FROM jsonb_array_elements(p_order_items)
LOOP
SELECT * INTO v_product_record
FROM products
WHERE id = (v_item->>'product_id')::UUID
AND availability_status = true;
IF NOT FOUND THEN
RAISE EXCEPTION 'Product % not available', v_item->>'name';
END IF;
-- 檢查庫存
IF v_product_record.stock_quantity < (v_item->>'quantity')::INTEGER THEN
RAISE EXCEPTION 'Insufficient stock for %', v_item->>'name';
END IF;
-- 累計總金額
v_total_amount := v_total_amount + (v_item->>'subtotal')::DECIMAL(10,2);
-- 更新庫存
UPDATE products
SET stock_quantity = stock_quantity - (v_item->>'quantity')::INTEGER
WHERE id = v_product_record.id;
END LOOP;
-- 處理付款
IF p_payment_method = 'wallet' THEN
IF v_user_record.wallet_balance < v_total_amount THEN
RAISE EXCEPTION 'Insufficient wallet balance';
END IF;
UPDATE users
SET wallet_balance = wallet_balance - v_total_amount
WHERE id = p_user_id;
ELSIF p_payment_method = 'points' THEN
IF v_user_record.points_balance < v_total_amount THEN
RAISE EXCEPTION 'Insufficient points balance';
END IF;
UPDATE users
SET points_balance = points_balance - v_total_amount::INTEGER
WHERE id = p_user_id;
END IF;
-- 計算預估取餐時間
IF p_order_type = 'pickup_now' THEN
v_estimated_pickup := NOW() + INTERVAL '1 minute' * (v_store_record.current_queue_count * 5 + 10);
ELSE
v_estimated_pickup := p_scheduled_time;
END IF;
-- 創建訂單
INSERT INTO orders (
user_id,
store_id,
order_items,
total_amount,
payment_method,
order_type,
scheduled_time,
estimated_pickup_time,
special_instructions
) VALUES (
p_user_id,
p_store_id,
p_order_items,
v_total_amount,
p_payment_method,
p_order_type,
p_scheduled_time,
v_estimated_pickup,
p_special_instructions
) RETURNING id INTO v_order_id;
-- 更新門市排隊狀況
IF p_order_type = 'pickup_now' THEN
UPDATE stores
SET current_queue_count = current_queue_count + 1
WHERE id = p_store_id;
END IF;
-- 計算並給予點數回饋
v_points_earned := CASE v_user_record.membership_level
WHEN 'bronze' THEN (v_total_amount * 0.01)::INTEGER
WHEN 'silver' THEN (v_total_amount * 0.015)::INTEGER
WHEN 'gold' THEN (v_total_amount * 0.02)::INTEGER
ELSE 0
END;
UPDATE users
SET points_balance = points_balance + v_points_earned
WHERE id = p_user_id;
-- 返回結果
RETURN jsonb_build_object(
'success', true,
'order_id', v_order_id,
'total_amount', v_total_amount,
'points_earned', v_points_earned,
'estimated_pickup_time', v_estimated_pickup
);
EXCEPTION WHEN OTHERS THEN
-- 回滾所有變更
RAISE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
🔗 第四步:即時功能實作
1. 即時訂單狀態更新
前端即時訂閱:
// hooks/useRealtimeOrders.ts
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
export function useRealtimeOrders(userId: string) {
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 初始載入訂單
loadOrders();
// 設定即時訂閱
const channel = supabase
.channel(`orders-${userId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'orders',
filter: `user_id=eq.${userId}`
},
(payload) => {
handleOrderUpdate(payload);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [userId]);
const loadOrders = async () => {
try {
const data = await OrderService.getUserOrders(userId, { limit: 20 });
setOrders(data);
} catch (error) {
console.error('載入訂單失敗:', error);
} finally {
setLoading(false);
}
};
const handleOrderUpdate = (payload: any) => {
const { eventType, new: newOrder, old: oldOrder } = payload;
setOrders(prevOrders => {
switch (eventType) {
case 'INSERT':
return [newOrder, ...prevOrders];
case 'UPDATE':
return prevOrders.map(order =>
order.id === newOrder.id ? { ...order, ...newOrder } : order
);
case 'DELETE':
return prevOrders.filter(order => order.id !== oldOrder.id);
default:
return prevOrders;
}
});
// 顯示狀態變更通知
if (eventType === 'UPDATE' && oldOrder.order_status !== newOrder.order_status) {
showOrderStatusNotification(newOrder);
}
};
const showOrderStatusNotification = (order: Order) => {
const statusMessages = {
confirmed: '訂單已確認!',
preparing: '開始製作中...',
ready: '餐點已準備完成,請前往取餐',
completed: '感謝您的訂購!',
cancelled: '訂單已取消'
};
// 顯示 toast 通知或推播
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('茶語時光', {
body: `訂單 ${order.order_number}: ${statusMessages[order.order_status]}`,
icon: '/icons/tea-icon.png'
});
}
};
return { orders, loading, refetch: loadOrders };
}
2. 即時庫存更新
商品庫存即時監控:
// hooks/useRealtimeInventory.ts
export function useRealtimeInventory(productIds: string[]) {
const [inventory, setInventory] = useState<Record<string, number>>({});
useEffect(() => {
const channel = supabase
.channel('inventory-updates')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'products',
filter: `id=in.(${productIds.join(',')})`
},
(payload) => {
const { new: updatedProduct } = payload;
setInventory(prev => ({
...prev,
[updatedProduct.id]: updatedProduct.stock_quantity
}));
// 低庫存警示
if (updatedProduct.stock_quantity <= updatedProduct.low_stock_threshold) {
showLowStockAlert(updatedProduct);
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [productIds]);
return inventory;
}
3. 即時門市排隊狀況
門市排隊即時更新:
// hooks/useRealtimeQueue.ts
export function useRealtimeQueue(storeId: string) {
const [queueData, setQueueData] = useState({
currentCount: 0,
averageWait: 0,
estimatedWait: 0
});
useEffect(() => {
const channel = supabase
.channel(`queue-${storeId}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'stores',
filter: `id=eq.${storeId}`
},
(payload) => {
const { new: updatedStore } = payload;
setQueueData({
currentCount: updatedStore.current_queue_count,
averageWait: updatedStore.average_wait_time,
estimatedWait: updatedStore.current_queue_count * 5
});
}
)
.subscribe();
// 初始載入
loadInitialQueueData();
return () => {
supabase.removeChannel(channel);
};
}, [storeId]);
const loadInitialQueueData = async () => {
const { data } = await supabase
.from('stores')
.select('current_queue_count, average_wait_time')
.eq('id', storeId)
.single();
if (data) {
setQueueData({
currentCount: data.current_queue_count,
averageWait: data.average_wait_time,
estimatedWait: data.current_queue_count * 5
});
}
};
return queueData;
}
🛡️ 第五步:安全性與權限控制
1. Row Level Security (RLS) 進階設定
細粒度權限控制:
-- 用戶表 RLS 政策增強
CREATE POLICY "Users can view own profile" ON users
FOR SELECT USING (auth.uid() = id OR auth.jwt() ->> 'line_user_id' = line_user_id);
CREATE POLICY "Users can update own profile" ON users
FOR UPDATE USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
-- 管理員可以查看所有用戶
CREATE POLICY "Admins can view all users" ON users
FOR SELECT USING (auth.jwt() ->> 'role' = 'admin');
-- 訂單表 RLS 政策
CREATE POLICY "Users can view own orders" ON orders
FOR SELECT USING (user_id = auth.uid());
CREATE POLICY "Users can create orders" ON orders
FOR INSERT WITH CHECK (user_id = auth.uid());
-- 店員可以查看和更新店內訂單
CREATE POLICY "Store staff can manage store orders" ON orders
FOR ALL USING (
store_id IN (
SELECT id FROM stores
WHERE manager_id = auth.uid()
OR auth.jwt() ->> 'role' = 'store_staff'
)
);
-- 商品表 RLS - 分層權限
CREATE POLICY "Everyone can view available products" ON products
FOR SELECT USING (availability_status = true);
CREATE POLICY "Store managers can update own store products" ON products
FOR UPDATE USING (
auth.jwt() ->> 'role' IN ('admin', 'store_manager')
);
2. 自定義認證 Hook
JWT Token 處理:
// hooks/useAuth.ts
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
export function useAuth() {
const [user, setUser] = useState(null);
const [session, setSession] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 取得目前 session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user ?? null);
setLoading(false);
});
// 監聽認證狀態變更
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
setSession(session);
setUser(session?.user ?? null);
setLoading(false);
// 同步用戶資料
if (session?.user && event === 'SIGNED_IN') {
await syncUserData(session.user);
}
}
);
return () => subscription.unsubscribe();
}, []);
const signInWithLineToken = async (lineToken: string, userProfile: any) => {
try {
// 使用自定義認證函數
const { data, error } = await supabase.auth.signInWithIdToken({
provider: 'custom',
token: lineToken,
options: {
userData: userProfile
}
});
if (error) throw error;
return data;
} catch (error) {
console.error('LINE 認證失敗:', error);
throw error;
}
};
const syncUserData = async (authUser: any) => {
try {
const { error } = await supabase
.from('users')
.upsert({
id: authUser.id,
line_user_id: authUser.user_metadata?.line_user_id,
display_name: authUser.user_metadata?.display_name,
picture_url: authUser.user_metadata?.picture_url,
last_login: new Date().toISOString()
}, {
onConflict: 'line_user_id'
});
if (error) console.error('用戶資料同步失敗:', error);
} catch (error) {
console.error('同步錯誤:', error);
}
};
return {
user,
session,
loading,
signInWithLineToken,
signOut: () => supabase.auth.signOut()
};
}
3. API 安全中間件
Edge Functions 安全檢查:
// supabase/functions/secure-api/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req) => {
try {
// CORS 處理
if (req.method === 'OPTIONS') {
return new Response('ok', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
},
});
}
// 驗證 Authorization header
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Missing authorization header' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
// 初始化 Supabase client
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: authHeader },
},
}
);
// 驗證用戶
const { data: { user }, error: authError } = await supabaseClient.auth.getUser();
if (authError || !user) {
return new Response(
JSON.stringify({ error: 'Invalid token' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
// 處理業務邏輯
const { method, url } = req;
const urlPath = new URL(url).pathname;
switch (method) {
case 'POST':
if (urlPath.includes('/orders')) {
return await handleCreateOrder(req, supabaseClient, user);
}
break;
case 'GET':
if (urlPath.includes('/analytics')) {
return await handleAnalytics(req, supabaseClient, user);
}
break;
}
return new Response(
JSON.stringify({ error: 'Not found' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
});
async function handleCreateOrder(req: Request, supabase: any, user: any) {
const body = await req.json();
// 額外的業務邏輯驗證
if (!body.store_id || !body.items || body.items.length === 0) {
return new Response(
JSON.stringify({ error: 'Invalid order data' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// 呼叫資料庫函數
const { data, error } = await supabase.rpc('create_order_transaction', {
p_user_id: user.id,
...body
});
if (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
return new Response(
JSON.stringify({ success: true, data }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
📊 第六步:分析與監控
1. 業務分析儀表板
建立分析檢視:
-- 創建業務分析檢視
CREATE VIEW daily_business_metrics AS
SELECT
DATE(created_at) as date,
COUNT(*) as total_orders,
SUM(final_amount) as total_revenue,
AVG(final_amount) as avg_order_value,
COUNT(DISTINCT user_id) as unique_customers,
COUNT(*) FILTER (WHERE order_status = 'completed') as completed_orders,
COUNT(*) FILTER (WHERE order_status = 'cancelled') as cancelled_orders,
ROUND(
COUNT(*) FILTER (WHERE order_status = 'completed') * 100.0 / COUNT(*), 2
) as completion_rate
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE(created_at)
ORDER BY date DESC;
-- 產品熱銷分析
CREATE VIEW product_popularity AS
SELECT
p.name,
p.category,
COUNT(oi.product_id) as order_count,
SUM(oi.quantity) as total_quantity,
SUM(oi.subtotal) as total_revenue,
AVG(p.rating) as avg_rating
FROM products p
JOIN (
SELECT
(jsonb_array_elements(order_items)->>'product_id')::UUID as product_id,
(jsonb_array_elements(order_items)->>'quantity')::INTEGER as quantity,
(jsonb_array_elements(order_items)->>'subtotal')::DECIMAL as subtotal
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days'
AND order_status = 'completed'
) oi ON p.id = oi.product_id
GROUP BY p.id, p.name, p.category
ORDER BY total_quantity DESC;
-- 門市表現分析
CREATE VIEW store_performance AS
SELECT
s.name as store_name,
COUNT(o.id) as total_orders,
SUM(o.final_amount) as revenue,
AVG(EXTRACT(EPOCH FROM (o.actual_pickup_time - o.created_at))/60) as avg_fulfillment_minutes,
COUNT(*) FILTER (WHERE o.order_status = 'completed') as completed_orders,
ROUND(
COUNT(*) FILTER (WHERE o.order_status = 'completed') * 100.0 / COUNT(*), 2
) as completion_rate
FROM stores s
LEFT JOIN orders o ON s.id = o.store_id
WHERE o.created_at >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY s.id, s.name
ORDER BY revenue DESC;
分析 API 端點:
// services/analyticsService.ts
export class AnalyticsService {
// 即時業務指標
static async getRealtimeMetrics() {
const { data, error } = await supabase
.rpc('get_realtime_business_metrics');
if (error) throw error;
return data;
}
// 銷售趨勢分析
static async getSalesTrends(days: number = 30) {
const { data, error } = await supabase
.from('daily_business_metrics')
.select('*')
.gte('date', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0])
.order('date', { ascending: true });
if (error) throw error;
return data;
}
// 產品表現分析
static async getProductAnalytics(timeframe: 'week' | 'month' = 'week') {
const { data, error } = await supabase
.from('product_popularity')
.select('*')
.limit(20);
if (error) throw error;
return data;
}
// 用戶行為分析
static async getUserBehaviorMetrics() {
const { data, error } = await supabase
.rpc('analyze_user_behavior');
if (error) throw error;
return data;
}
}
2. 即時監控警示
系統健康監控:
-- 建立系統健康檢查函數
CREATE OR REPLACE FUNCTION system_health_check()
RETURNS JSONB AS $$
DECLARE
result JSONB;
failed_orders INTEGER;
low_stock_products INTEGER;
high_queue_stores INTEGER;
BEGIN
-- 檢查失敗訂單
SELECT COUNT(*) INTO failed_orders
FROM orders
WHERE order_status = 'failed'
AND created_at > NOW() - INTERVAL '1 hour';
-- 檢查低庫存商品
SELECT COUNT(*) INTO low_stock_products
FROM products
WHERE stock_quantity <= low_stock_threshold
AND availability_status = true;
-- 檢查排隊過長的門市
SELECT COUNT(*) INTO high_queue_stores
FROM stores
WHERE current_queue_count > max_concurrent_orders * 0.9;
result := jsonb_build_object(
'timestamp', NOW(),
'status', CASE
WHEN failed_orders > 5 OR low_stock_products > 10 OR high_queue_stores > 0
THEN 'warning'
ELSE 'healthy'
END,
'metrics', jsonb_build_object(
'failed_orders_last_hour', failed_orders,
'low_stock_products', low_stock_products,
'congested_stores', high_queue_stores
)
);
RETURN result;
END;
$$ LANGUAGE plpgsql;
3. 效能監控
查詢效能優化:
-- 建立效能監控檢視
CREATE VIEW slow_queries AS
SELECT
query,
calls,
total_time,
mean_time,
rows
FROM pg_stat_statements
WHERE mean_time > 100 -- 超過 100ms 的查詢
ORDER BY mean_time DESC;
-- 資料庫連接監控
CREATE VIEW connection_stats AS
SELECT
datname,
numbackends,
xact_commit,
xact_rollback,
blks_read,
blks_hit,
tup_returned,
tup_fetched,
tup_inserted,
tup_updated,
tup_deleted
FROM pg_stat_database
WHERE datname = current_database();
💰 第七步:成本效益與部署
1. Supabase 定價分析
Supabase 定價方案對比
從免費開始,隨業務成長彈性升級
Free
適合個人專案與 MVP 驗證
🗄️ 資料庫
- • 儲存空間: 500 MB
- • 頻寬: 2 GB
- • 即時連線: 200 個同時連線
- • 備份: 7 天自動備份
🔐 認證
- • 活躍用戶: 50,000 MAU
- • 社群登入: ✅ 支援
- • SSO: ❌
⚡ Edge Functions
- • 調用次數: 500,000 次/月
- • 執行時間: 40 小時/月
🎧 支援
社群支援
Pro
適合生產環境與小型團隊
🗄️ 資料庫
- • 儲存空間: 8 GB 包含,之後 $0.125/GB
- • 頻寬: 50 GB 包含,之後 $0.09/GB
- • 即時連線: 500 個同時連線
- • 備份: 30 天自動備份
🔐 認證
- • 活躍用戶: 100,000 MAU
- • 社群登入: ✅ 支援
- • SSO: ❌
⚡ Edge Functions
- • 調用次數: 2,000,000 次/月
- • 執行時間: 150 小時/月
🎧 支援
Email 支援
Team
適合成長中的團隊與企業
🗄️ 資料庫
- • 儲存空間: 8 GB 包含,之後 $0.125/GB
- • 頻寬: 250 GB 包含,之後 $0.09/GB
- • 即時連線: 1,500 個同時連線
- • 備份: 30 天自動備份 + PITR
🔐 認證
- • 活躍用戶: 無限制
- • 社群登入: ✅ 支援
- • SSO: ✅ SAML 2.0
⚡ Edge Functions
- • 調用次數: 10,000,000 次/月
- • 執行時間: 1,000 小時/月
🎧 支援
優先 Email + SLA 保證
🍵 茶語時光系統建議方案
🚀 MVP 階段
建議:Free 方案
- • 支援 500+ 活躍用戶
- • 完整功能驗證
- • 無需信用卡
- • 0 成本驗證市場
📈 成長階段
建議:Pro 方案 ($25/月)
- • 支援 5,000+ 活躍用戶
- • 完整商業功能
- • Email 技術支援
- • 性價比最高
🏢 企業階段
建議:Team 方案 ($599/月)
- • 無限用戶數
- • 企業級安全
- • SLA 保證
- • 優先支援
💡 所有方案都包含:PostgreSQL 資料庫、RESTful API、即時訂閱、認證系統、檔案儲存
📞 需要企業級支援或自部署方案?聯繫銷售團隊
2. 三平台成本比較
三平台成本效益分析
不同業務規模下的真實成本對比
成長階段 (5K-50K 用戶) - 月費用
Bubble.io
限制與特色
- •擴展成本高
- •效能瓶頸
- •平台依賴
Xano
限制與特色
- •API 調用計費
- •額外儲存費用
- •有限客製化
Supabase
限制與特色
- •按用量計費
- •可預測成本
- •完全控制
📊 年度成本預估 (成長階段 (5K-50K 用戶))
💰 投資回報率 (ROI) 分析
成本效益排名
選擇 Supabase 的節省
3. 部署策略
多環境部署:
# 開發環境
supabase start # 本地開發
supabase db reset # 重置資料庫
supabase db push # 推送 schema 變更
# 測試環境
supabase link --project-ref staging-project
supabase db push --linked # 推送到遠端
# 生產環境部署
supabase link --project-ref production-project
supabase db push --linked --confirm
# 資料遷移
supabase db dump --linked > backup.sql
supabase db reset --linked
psql -d postgres://... -f backup.sql
Edge Functions 部署:
# 部署 Edge Functions
supabase functions deploy order-processor
supabase functions deploy analytics-cron
supabase functions deploy notification-handler
# 設定環境變數
supabase secrets set LINE_CHANNEL_SECRET=your_secret
supabase secrets set NOTIFICATION_WEBHOOK=your_webhook
🏆 專案成果總結
技術成果對比
✅ Supabase 實現功能:
- 完整的 PostgreSQL 關聯式資料庫
- 自動生成的 RESTful API (20+ 端點)
- 即時 WebSocket 連接(訂單、庫存、排隊狀況)
- Row Level Security 細粒度權限控制
- Edge Functions 自定義業務邏輯
- 即時分析與監控儀表板
- 多環境開發部署流程
- 開源生態系統整合
三平台關鍵對比
最終決策矩陣分析
基於加權評分的客觀比較分析
評估項目 權重 | 🫧 Bubble.io | ⚡ Xano | 🚀 Supabase |
---|---|---|---|
資料庫架構 重要度: (9/10) | 6/10 簡化資料模型,限制複雜查詢 | 8/10 真正 PostgreSQL,支援複雜關係 | 10/10 原生 PostgreSQL + 即時功能 |
API 設計品質 重要度: (8/10) | 7/10 工作流程式 API,學習曲線低 | 9/10 標準 RESTful,開發者友善 | 10/10 自動生成 REST + GraphQL |
即時功能支援 重要度: (9/10) | 4/10 需額外開發,複雜度高 | 5/10 需第三方整合 | 10/10 原生 WebSocket,毫秒級同步 |
可擴展性 重要度: (8/10) | 5/10 ~100 並發用戶上限 | 7/10 ~10K 並發用戶 | 9/10 100K+ 並發用戶 |
開發者體驗 重要度: (7/10) | 9/10 視覺化,零程式碼 | 8/10 開發者友善的無程式碼 | 9/10 SQL + TypeScript,現代開發 |
廠商鎖定風險 重要度: (8/10) | 3/10 高度平台依賴 | 5/10 中度平台依賴 | 10/10 開源,可自部署 |
團隊接手容易度 重要度: (7/10) | 6/10 需學習平台特定知識 | 7/10 標準化程度較高 | 9/10 標準 SQL + JS,無學習門檻 |
長期維護性 重要度: (8/10) | 5/10 平台更新可能影響現有系統 | 7/10 相對穩定,但仍依賴平台 | 9/10 開源保障,社群維護 |
初期開發成本 重要度: (7/10) | 9/10 最低學習成本 | 8/10 中等學習成本 | 8/10 SQL 基礎需求 |
運營成本效益 重要度: (8/10) | 6/10 中等定價,功能限制多 | 7/10 較高定價,功能完整 | 9/10 最佳性價比,免費額度大 |
擴展成本 重要度: (8/10) | 4/10 擴展成本高,效能瓶頸 | 6/10 線性擴展成本 | 9/10 彈性計費,可自部署 |
最終建議:Supabase 以 9.3/10 分獲勝
決定性優勢:
- 技術領先 - 原生即時功能 + PostgreSQL 完整支援
- 開源保障 - 零廠商鎖定風險,可永久掌控數據
- 成本最優 - 免費額度慷慨,付費方案性價比最高
- 擴展性強 - 從 MVP 到企業級無縫擴展
- 生態完整 - TypeScript + SQL,團隊無學習成本
🍵 對茶語時光項目的具體價值:
🤔 但是,如果你的情況是...
• 需要 3 週內 MVP
• 預期用戶 < 1000 人
• 預算極有限
• 團隊已熟悉 Xano
• 不需要即時功能
• 中等規模應用(5K-50K 用戶)
• 重視即時功能
• 避免廠商鎖定
• 長期發展考量
• 50K+ 用戶規劃
Supabase 的決定性優勢
🔓 開源生態系統:
- 避免廠商鎖定,永久數據控制權
- 可自部署,完全自主掌控
- 活躍社群,持續功能增強
⚡ 即時能力領先:
- 原生 WebSocket 支援,無需額外配置
- 毫秒級資料同步,用戶體驗最佳
- 複雜即時業務邏輯實現能力
🛡️ 企業級安全:
- PostgreSQL 原生安全特性
- Row Level Security 細粒度控制
- 多重認證整合能力
💰 成本效益最優:
- 免費額度:100GB 資料庫 + 500MB 儲存
- 付費方案:$25/月起,比 Xano 便宜 40%
- 隨用量彈性計費,避免固定成本浪費
實際業務價值
開發效率提升:
- 比 Bubble 快 50%(1.5 週 vs 3 週)
- 比傳統開發快 75%(1.5 週 vs 6 週)
- 零維護負擔,專注業務邏輯
技術債務控制:
- 標準 SQL + TypeScript,團隊無學習成本
- 完整版本控制,代碼品質可控
- 開源保障,永無平台風險
擴展性保證:
- 支援 100,000+ 並發用戶
- 可無縫遷移到自部署環境
- 國際化部署能力
🎯 最佳實踐建議
開發流程建議
1. 本地優先開發:
# 完整本地開發環境
supabase start
supabase db reset
supabase db push
supabase functions serve
# 開發完成後推送
supabase db push --linked
supabase functions deploy
2. 漸進式遷移策略:
// 階段一:核心功能
- 用戶管理 + 商品目錄
- 基本訂單流程
// 階段二:即時功能
- 訂單狀態即時更新
- 庫存即時同步
// 階段三:進階功能
- 智慧分析儀表板
- 個人化推薦系統
3. 監控與優化:
-- 定期效能檢查
SELECT * FROM slow_queries WHERE mean_time > 100;
-- 索引優化建議
SELECT * FROM pg_stat_user_indexes WHERE idx_scan < 50;
-- 資料庫大小監控
SELECT pg_size_pretty(pg_database_size(current_database()));
團隊協作建議
角色分工:
- 全端開發:Supabase + React LIFF 整合
- 資料庫設計師:PostgreSQL schema 設計與優化
- DevOps 工程師:CI/CD 與多環境管理
- 業務分析師:SQL 查詢與分析儀表板
📞 學習資源與社群
Supabase 官方資源
學習平台:
- Supabase 官方文檔 - 完整技術指南
- Supabase University - 免費影片課程
- Supabase Blog - 最新功能與最佳實踐
開發工具:
- Supabase CLI - 本地開發工具
- Supabase Studio - 視覺化管理介面
- Template Gallery - 專案模板庫
中文學習社群
推薦社群:
- Supabase 台灣開發者 - Discord 社群
- PostgreSQL 台灣使用者群組 - Facebook 社團
- 全端開發者聯盟 - Telegram 群組
🍵 結語與選擇建議
經過三篇完整的實作指南分析,我們為"茶語時光"系統提供了三種截然不同的後端解決方案:
🎯 選擇決策矩陣
如果你是...
📱 快速驗證 MVP (選擇 Bubble.io):
- 非技術背景創業者
- 需要在 3 週內完成原型
- 預算有限,團隊 < 3 人
- 用戶量預期 < 1,000 人
🏢 中型企業應用 (選擇 Xano):
- 有基礎技術團隊
- 需要專業級 API 設計
- 預期用戶量 5,000-50,000 人
- 重視開發效率與成本控制
🚀 企業級長期發展 (選擇 Supabase):
- 技術團隊具備 SQL 能力
- 需要即時功能與高併發
- 預期用戶量 50,000+ 人
- 重視開源生態與避免廠商鎖定
🏆 Supabase 的勝出理由
在"茶語時光"這樣的茶飲預約系統中,Supabase 展現了最佳的整體價值:
- 即時訂單追蹤:原生 WebSocket 完美支援
- 複雜商業邏輯:PostgreSQL 函數 + Edge Functions
- 成本效益最優:免費額度慷慨,付費方案合理
- 技術債務最低:標準化技術棧,零學習成本
- 未來擴展性:開源保障,永無平台風險
🌟 最終建議
對於"茶語時光"這樣的現代化茶飲預約系統,Supabase 提供了最平衡的解決方案,結合了:
- Bubble 的開發效率
- Xano 的專業架構
- 加上獨有的即時能力與開源優勢
🚀 立即開始你的 Supabase 之旅,打造下一代即時互動的數位茶飲體驗!
本指南基於 Supabase 最新功能與企業級最佳實踐,提供可直接應用於生產環境的完整解決方案。從 MVP 到企業級擴展,Supabase 將陪伴你的業務成長每一步。
Interactive Components
This post includes custom interactive components for enhanced experience
Thanks for reading!
Found this article helpful? Share it with others or explore more content.