新增資料庫架構
This commit is contained in:
118
scripts/01-create-tables.sql
Normal file
118
scripts/01-create-tables.sql
Normal file
@@ -0,0 +1,118 @@
|
||||
-- 心願星河 - 基礎表格創建
|
||||
-- 執行順序:第 1 步
|
||||
-- 說明:創建應用程式所需的基礎數據表格
|
||||
|
||||
-- 開始事務
|
||||
BEGIN;
|
||||
|
||||
-- 1. 創建 wishes 表格(困擾案例主表)
|
||||
CREATE TABLE IF NOT EXISTS wishes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL CHECK (length(title) >= 1 AND length(title) <= 200),
|
||||
current_pain TEXT NOT NULL CHECK (length(current_pain) >= 1 AND length(current_pain) <= 2000),
|
||||
expected_solution TEXT NOT NULL CHECK (length(expected_solution) >= 1 AND length(expected_solution) <= 2000),
|
||||
expected_effect TEXT CHECK (length(expected_effect) <= 1000),
|
||||
is_public BOOLEAN DEFAULT true NOT NULL,
|
||||
email TEXT CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
||||
images JSONB DEFAULT '[]'::jsonb NOT NULL,
|
||||
user_session TEXT NOT NULL DEFAULT gen_random_uuid()::text,
|
||||
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deleted')),
|
||||
category TEXT CHECK (category IN ('工作效率', '人際關係', '技術問題', '職涯發展', '工作環境', '其他')),
|
||||
priority INTEGER DEFAULT 3 CHECK (priority >= 1 AND priority <= 5),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- 2. 創建 wish_likes 表格(點讚記錄)
|
||||
CREATE TABLE IF NOT EXISTS wish_likes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
wish_id BIGINT NOT NULL REFERENCES wishes(id) ON DELETE CASCADE,
|
||||
user_session TEXT NOT NULL,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
UNIQUE(wish_id, user_session)
|
||||
);
|
||||
|
||||
-- 3. 創建 user_settings 表格(用戶設定)
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_session TEXT UNIQUE NOT NULL,
|
||||
background_music_enabled BOOLEAN DEFAULT false NOT NULL,
|
||||
background_music_volume DECIMAL(3,2) DEFAULT 0.30 CHECK (background_music_volume >= 0 AND background_music_volume <= 1),
|
||||
background_music_playing BOOLEAN DEFAULT false NOT NULL,
|
||||
theme_preference TEXT DEFAULT 'auto' CHECK (theme_preference IN ('light', 'dark', 'auto')),
|
||||
language_preference TEXT DEFAULT 'zh-TW' CHECK (language_preference IN ('zh-TW', 'en-US')),
|
||||
notification_enabled BOOLEAN DEFAULT true NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- 4. 創建 migration_log 表格(遷移記錄)
|
||||
CREATE TABLE IF NOT EXISTS migration_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_session TEXT NOT NULL,
|
||||
migration_type TEXT NOT NULL CHECK (migration_type IN ('wishes', 'likes', 'settings')),
|
||||
source_data JSONB,
|
||||
target_records INTEGER DEFAULT 0,
|
||||
success BOOLEAN DEFAULT false NOT NULL,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- 5. 創建 system_stats 表格(系統統計)
|
||||
CREATE TABLE IF NOT EXISTS system_stats (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stat_date DATE DEFAULT CURRENT_DATE NOT NULL,
|
||||
total_wishes INTEGER DEFAULT 0,
|
||||
public_wishes INTEGER DEFAULT 0,
|
||||
private_wishes INTEGER DEFAULT 0,
|
||||
total_likes INTEGER DEFAULT 0,
|
||||
active_users INTEGER DEFAULT 0,
|
||||
storage_used_mb DECIMAL(10,2) DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
UNIQUE(stat_date)
|
||||
);
|
||||
|
||||
-- 提交事務
|
||||
COMMIT;
|
||||
|
||||
-- 添加表格註釋
|
||||
COMMENT ON TABLE wishes IS '用戶提交的工作困擾案例主表';
|
||||
COMMENT ON TABLE wish_likes IS '困擾案例的點讚記錄表';
|
||||
COMMENT ON TABLE user_settings IS '用戶個人設定表(音樂、主題等)';
|
||||
COMMENT ON TABLE migration_log IS '數據遷移記錄表';
|
||||
COMMENT ON TABLE system_stats IS '系統統計數據表';
|
||||
|
||||
-- 添加欄位註釋
|
||||
COMMENT ON COLUMN wishes.title IS '困擾案例標題';
|
||||
COMMENT ON COLUMN wishes.current_pain IS '目前遇到的困擾描述';
|
||||
COMMENT ON COLUMN wishes.expected_solution IS '期望的解決方案';
|
||||
COMMENT ON COLUMN wishes.expected_effect IS '預期效果描述';
|
||||
COMMENT ON COLUMN wishes.is_public IS '是否公開顯示';
|
||||
COMMENT ON COLUMN wishes.images IS '相關圖片資訊(JSON格式)';
|
||||
COMMENT ON COLUMN wishes.user_session IS '用戶會話標識';
|
||||
COMMENT ON COLUMN wishes.category IS '困擾類別';
|
||||
COMMENT ON COLUMN wishes.priority IS '優先級(1-5,5最高)';
|
||||
|
||||
COMMENT ON COLUMN wish_likes.wish_id IS '被點讚的困擾案例ID';
|
||||
COMMENT ON COLUMN wish_likes.user_session IS '點讚用戶的會話標識';
|
||||
COMMENT ON COLUMN wish_likes.ip_address IS '點讚用戶的IP地址';
|
||||
|
||||
COMMENT ON COLUMN user_settings.background_music_enabled IS '背景音樂是否啟用';
|
||||
COMMENT ON COLUMN user_settings.background_music_volume IS '背景音樂音量(0-1)';
|
||||
COMMENT ON COLUMN user_settings.theme_preference IS '主題偏好設定';
|
||||
|
||||
-- 顯示創建結果
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '✅ 基礎表格創建完成!';
|
||||
RAISE NOTICE '📊 創建的表格:';
|
||||
RAISE NOTICE ' - wishes(困擾案例)';
|
||||
RAISE NOTICE ' - wish_likes(點讚記錄)';
|
||||
RAISE NOTICE ' - user_settings(用戶設定)';
|
||||
RAISE NOTICE ' - migration_log(遷移記錄)';
|
||||
RAISE NOTICE ' - system_stats(系統統計)';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🔄 下一步:執行 02-create-indexes.sql';
|
||||
END $$;
|
174
scripts/02-create-indexes.sql
Normal file
174
scripts/02-create-indexes.sql
Normal file
@@ -0,0 +1,174 @@
|
||||
-- 心願星河 - 索引和觸發器創建
|
||||
-- 執行順序:第 2 步
|
||||
-- 說明:創建性能優化索引和自動更新觸發器
|
||||
|
||||
-- 開始事務
|
||||
BEGIN;
|
||||
|
||||
-- 1. wishes 表格索引
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_created_at ON wishes(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_is_public ON wishes(is_public) WHERE is_public = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_status ON wishes(status) WHERE status = 'active';
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_category ON wishes(category) WHERE category IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_priority ON wishes(priority DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_user_session ON wishes(user_session);
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_email ON wishes(email) WHERE email IS NOT NULL;
|
||||
|
||||
-- 全文搜索索引 (使用 simple 配置以支持多语言)
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_search ON wishes USING gin(
|
||||
to_tsvector('simple', title || ' ' || current_pain || ' ' || expected_solution)
|
||||
);
|
||||
|
||||
-- 2. wish_likes 表格索引
|
||||
CREATE INDEX IF NOT EXISTS idx_wish_likes_wish_id ON wish_likes(wish_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_wish_likes_user_session ON wish_likes(user_session);
|
||||
CREATE INDEX IF NOT EXISTS idx_wish_likes_created_at ON wish_likes(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_wish_likes_ip_address ON wish_likes(ip_address);
|
||||
|
||||
-- 3. user_settings 表格索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_settings_session ON user_settings(user_session);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_settings_updated_at ON user_settings(updated_at DESC);
|
||||
|
||||
-- 4. migration_log 表格索引
|
||||
CREATE INDEX IF NOT EXISTS idx_migration_log_user_session ON migration_log(user_session);
|
||||
CREATE INDEX IF NOT EXISTS idx_migration_log_type ON migration_log(migration_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_migration_log_success ON migration_log(success);
|
||||
CREATE INDEX IF NOT EXISTS idx_migration_log_created_at ON migration_log(created_at DESC);
|
||||
|
||||
-- 5. system_stats 表格索引
|
||||
CREATE INDEX IF NOT EXISTS idx_system_stats_date ON system_stats(stat_date DESC);
|
||||
|
||||
-- 6. 創建更新時間觸發器函數
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 7. 為需要的表格添加更新時間觸發器
|
||||
DROP TRIGGER IF EXISTS update_wishes_updated_at ON wishes;
|
||||
CREATE TRIGGER update_wishes_updated_at
|
||||
BEFORE UPDATE ON wishes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
DROP TRIGGER IF EXISTS update_user_settings_updated_at ON user_settings;
|
||||
CREATE TRIGGER update_user_settings_updated_at
|
||||
BEFORE UPDATE ON user_settings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 8. 創建統計更新觸發器函數
|
||||
CREATE OR REPLACE FUNCTION update_system_stats()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO system_stats (
|
||||
stat_date,
|
||||
total_wishes,
|
||||
public_wishes,
|
||||
private_wishes,
|
||||
total_likes,
|
||||
active_users
|
||||
)
|
||||
SELECT
|
||||
CURRENT_DATE,
|
||||
COUNT(*) as total_wishes,
|
||||
COUNT(*) FILTER (WHERE is_public = true) as public_wishes,
|
||||
COUNT(*) FILTER (WHERE is_public = false) as private_wishes,
|
||||
(SELECT COUNT(*) FROM wish_likes) as total_likes,
|
||||
COUNT(DISTINCT user_session) as active_users
|
||||
FROM wishes
|
||||
WHERE status = 'active'
|
||||
ON CONFLICT (stat_date)
|
||||
DO UPDATE SET
|
||||
total_wishes = EXCLUDED.total_wishes,
|
||||
public_wishes = EXCLUDED.public_wishes,
|
||||
private_wishes = EXCLUDED.private_wishes,
|
||||
total_likes = EXCLUDED.total_likes,
|
||||
active_users = EXCLUDED.active_users;
|
||||
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 9. 為 wishes 和 wish_likes 添加統計更新觸發器
|
||||
DROP TRIGGER IF EXISTS update_stats_on_wish_change ON wishes;
|
||||
CREATE TRIGGER update_stats_on_wish_change
|
||||
AFTER INSERT OR UPDATE OR DELETE ON wishes
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION update_system_stats();
|
||||
|
||||
DROP TRIGGER IF EXISTS update_stats_on_like_change ON wish_likes;
|
||||
CREATE TRIGGER update_stats_on_like_change
|
||||
AFTER INSERT OR DELETE ON wish_likes
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION update_system_stats();
|
||||
|
||||
-- 10. 創建圖片清理觸發器函數
|
||||
CREATE OR REPLACE FUNCTION cleanup_wish_images()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- 當 wish 被刪除時,記錄需要清理的圖片
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
INSERT INTO migration_log (
|
||||
user_session,
|
||||
migration_type,
|
||||
source_data,
|
||||
success,
|
||||
error_message
|
||||
) VALUES (
|
||||
OLD.user_session,
|
||||
'image_cleanup',
|
||||
OLD.images,
|
||||
false,
|
||||
'Images marked for cleanup'
|
||||
);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 11. 為 wishes 添加圖片清理觸發器
|
||||
DROP TRIGGER IF EXISTS cleanup_images_on_wish_delete ON wishes;
|
||||
CREATE TRIGGER cleanup_images_on_wish_delete
|
||||
AFTER DELETE ON wishes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION cleanup_wish_images();
|
||||
|
||||
-- 提交事務
|
||||
COMMIT;
|
||||
|
||||
-- 顯示創建結果
|
||||
DO $$
|
||||
DECLARE
|
||||
index_count INTEGER;
|
||||
trigger_count INTEGER;
|
||||
BEGIN
|
||||
-- 計算索引數量
|
||||
SELECT COUNT(*) INTO index_count
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
AND indexname LIKE 'idx_%';
|
||||
|
||||
-- 計算觸發器數量
|
||||
SELECT COUNT(*) INTO trigger_count
|
||||
FROM pg_trigger
|
||||
WHERE tgname LIKE '%wish%' OR tgname LIKE '%update%';
|
||||
|
||||
RAISE NOTICE '✅ 索引和觸發器創建完成!';
|
||||
RAISE NOTICE '📊 創建統計:';
|
||||
RAISE NOTICE ' - 性能索引:% 個', index_count;
|
||||
RAISE NOTICE ' - 自動觸發器:% 個', trigger_count;
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🚀 性能優化功能:';
|
||||
RAISE NOTICE ' - 快速查詢索引';
|
||||
RAISE NOTICE ' - 全文搜索支援';
|
||||
RAISE NOTICE ' - 自動統計更新';
|
||||
RAISE NOTICE ' - 圖片清理追蹤';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🔄 下一步:執行 03-create-views-functions.sql';
|
||||
END $$;
|
376
scripts/03-create-views-functions.sql
Normal file
376
scripts/03-create-views-functions.sql
Normal file
@@ -0,0 +1,376 @@
|
||||
-- 心願星河 - 視圖和函數創建
|
||||
-- 執行順序:第 3 步
|
||||
-- 說明:創建便利視圖和業務邏輯函數
|
||||
|
||||
-- 開始事務
|
||||
BEGIN;
|
||||
|
||||
-- 1. 創建帶點讚數的困擾案例視圖
|
||||
CREATE OR REPLACE VIEW wishes_with_likes AS
|
||||
SELECT
|
||||
w.*,
|
||||
COALESCE(like_counts.like_count, 0) as like_count,
|
||||
CASE
|
||||
WHEN w.created_at >= NOW() - INTERVAL '24 hours' THEN 'new'
|
||||
WHEN like_counts.like_count >= 10 THEN 'popular'
|
||||
WHEN w.priority >= 4 THEN 'urgent'
|
||||
ELSE 'normal'
|
||||
END as badge_type
|
||||
FROM wishes w
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
wish_id,
|
||||
COUNT(*) as like_count
|
||||
FROM wish_likes
|
||||
GROUP BY wish_id
|
||||
) like_counts ON w.id = like_counts.wish_id
|
||||
WHERE w.status = 'active';
|
||||
|
||||
-- 2. 創建公開困擾案例視圖
|
||||
CREATE OR REPLACE VIEW public_wishes AS
|
||||
SELECT *
|
||||
FROM wishes_with_likes
|
||||
WHERE is_public = true
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- 3. 創建熱門困擾案例視圖
|
||||
CREATE OR REPLACE VIEW popular_wishes AS
|
||||
SELECT *
|
||||
FROM wishes_with_likes
|
||||
WHERE is_public = true
|
||||
AND like_count >= 3
|
||||
ORDER BY like_count DESC, created_at DESC;
|
||||
|
||||
-- 4. 創建統計摘要視圖
|
||||
CREATE OR REPLACE VIEW wishes_summary AS
|
||||
SELECT
|
||||
COUNT(*) as total_wishes,
|
||||
COUNT(*) FILTER (WHERE is_public = true) as public_wishes,
|
||||
COUNT(*) FILTER (WHERE is_public = false) as private_wishes,
|
||||
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '7 days') as this_week,
|
||||
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '14 days' AND created_at < NOW() - INTERVAL '7 days') as last_week,
|
||||
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '24 hours') as today,
|
||||
AVG(COALESCE(like_counts.like_count, 0))::DECIMAL(10,2) as avg_likes,
|
||||
COUNT(DISTINCT user_session) as unique_users
|
||||
FROM wishes w
|
||||
LEFT JOIN (
|
||||
SELECT wish_id, COUNT(*) as like_count
|
||||
FROM wish_likes
|
||||
GROUP BY wish_id
|
||||
) like_counts ON w.id = like_counts.wish_id
|
||||
WHERE w.status = 'active';
|
||||
|
||||
-- 5. 創建類別統計視圖
|
||||
CREATE OR REPLACE VIEW category_stats AS
|
||||
SELECT
|
||||
COALESCE(category, '未分類') as category,
|
||||
COUNT(*) as wish_count,
|
||||
COUNT(*) FILTER (WHERE is_public = true) as public_count,
|
||||
AVG(COALESCE(like_counts.like_count, 0))::DECIMAL(10,2) as avg_likes,
|
||||
MAX(created_at) as latest_wish
|
||||
FROM wishes w
|
||||
LEFT JOIN (
|
||||
SELECT wish_id, COUNT(*) as like_count
|
||||
FROM wish_likes
|
||||
GROUP BY wish_id
|
||||
) like_counts ON w.id = like_counts.wish_id
|
||||
WHERE w.status = 'active'
|
||||
GROUP BY category
|
||||
ORDER BY wish_count DESC;
|
||||
|
||||
-- 6. 創建獲取統計數據的函數
|
||||
CREATE OR REPLACE FUNCTION get_wishes_stats()
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
result JSON;
|
||||
BEGIN
|
||||
SELECT json_build_object(
|
||||
'summary', (SELECT row_to_json(wishes_summary.*) FROM wishes_summary),
|
||||
'categories', (
|
||||
SELECT json_agg(row_to_json(category_stats.*))
|
||||
FROM category_stats
|
||||
),
|
||||
'recent_activity', (
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'date', date_trunc('day', created_at),
|
||||
'count', count(*)
|
||||
)
|
||||
)
|
||||
FROM wishes
|
||||
WHERE created_at >= NOW() - INTERVAL '30 days'
|
||||
AND status = 'active'
|
||||
GROUP BY date_trunc('day', created_at)
|
||||
ORDER BY date_trunc('day', created_at) DESC
|
||||
LIMIT 30
|
||||
)
|
||||
) INTO result;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 7. 創建搜索困擾案例的函數
|
||||
CREATE OR REPLACE FUNCTION search_wishes(
|
||||
search_query TEXT,
|
||||
limit_count INTEGER DEFAULT 20,
|
||||
offset_count INTEGER DEFAULT 0
|
||||
)
|
||||
RETURNS TABLE(
|
||||
id BIGINT,
|
||||
title TEXT,
|
||||
current_pain TEXT,
|
||||
expected_solution TEXT,
|
||||
like_count BIGINT,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
relevance REAL
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
w.id,
|
||||
w.title,
|
||||
w.current_pain,
|
||||
w.expected_solution,
|
||||
COALESCE(like_counts.like_count, 0) as like_count,
|
||||
w.created_at,
|
||||
ts_rank(
|
||||
to_tsvector('chinese', w.title || ' ' || w.current_pain || ' ' || w.expected_solution),
|
||||
plainto_tsquery('chinese', search_query)
|
||||
) as relevance
|
||||
FROM wishes w
|
||||
LEFT JOIN (
|
||||
SELECT wish_id, COUNT(*) as like_count
|
||||
FROM wish_likes
|
||||
GROUP BY wish_id
|
||||
) like_counts ON w.id = like_counts.wish_id
|
||||
WHERE w.status = 'active'
|
||||
AND w.is_public = true
|
||||
AND (
|
||||
to_tsvector('chinese', w.title || ' ' || w.current_pain || ' ' || w.expected_solution)
|
||||
@@ plainto_tsquery('chinese', search_query)
|
||||
)
|
||||
ORDER BY relevance DESC, like_count DESC, w.created_at DESC
|
||||
LIMIT limit_count
|
||||
OFFSET offset_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 8. 創建獲取用戶統計的函數
|
||||
CREATE OR REPLACE FUNCTION get_user_stats(session_id TEXT)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
result JSON;
|
||||
BEGIN
|
||||
SELECT json_build_object(
|
||||
'total_wishes', (
|
||||
SELECT COUNT(*)
|
||||
FROM wishes
|
||||
WHERE user_session = session_id AND status = 'active'
|
||||
),
|
||||
'total_likes_received', (
|
||||
SELECT COALESCE(SUM(like_counts.like_count), 0)
|
||||
FROM wishes w
|
||||
LEFT JOIN (
|
||||
SELECT wish_id, COUNT(*) as like_count
|
||||
FROM wish_likes
|
||||
GROUP BY wish_id
|
||||
) like_counts ON w.id = like_counts.wish_id
|
||||
WHERE w.user_session = session_id AND w.status = 'active'
|
||||
),
|
||||
'total_likes_given', (
|
||||
SELECT COUNT(*)
|
||||
FROM wish_likes
|
||||
WHERE user_session = session_id
|
||||
),
|
||||
'recent_wishes', (
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'id', id,
|
||||
'title', title,
|
||||
'created_at', created_at,
|
||||
'like_count', COALESCE(like_counts.like_count, 0)
|
||||
)
|
||||
)
|
||||
FROM wishes w
|
||||
LEFT JOIN (
|
||||
SELECT wish_id, COUNT(*) as like_count
|
||||
FROM wish_likes
|
||||
GROUP BY wish_id
|
||||
) like_counts ON w.id = like_counts.wish_id
|
||||
WHERE w.user_session = session_id
|
||||
AND w.status = 'active'
|
||||
ORDER BY w.created_at DESC
|
||||
LIMIT 5
|
||||
)
|
||||
) INTO result;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 9. 創建清理孤立圖片的函數
|
||||
CREATE OR REPLACE FUNCTION cleanup_orphaned_images()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
deleted_count INTEGER := 0;
|
||||
image_record RECORD;
|
||||
BEGIN
|
||||
-- 記錄清理開始
|
||||
INSERT INTO migration_log (
|
||||
user_session,
|
||||
migration_type,
|
||||
success,
|
||||
error_message
|
||||
) VALUES (
|
||||
'system',
|
||||
'image_cleanup',
|
||||
false,
|
||||
'Starting orphaned image cleanup'
|
||||
);
|
||||
|
||||
-- 這裡只是標記,實際的 Storage 清理需要在應用層面處理
|
||||
-- 因為 SQL 無法直接操作 Supabase Storage
|
||||
|
||||
-- 找出需要清理的圖片記錄
|
||||
FOR image_record IN
|
||||
SELECT DISTINCT jsonb_array_elements(images)->>'storage_path' as image_path
|
||||
FROM wishes
|
||||
WHERE status = 'deleted'
|
||||
AND images IS NOT NULL
|
||||
AND jsonb_array_length(images) > 0
|
||||
LOOP
|
||||
-- 標記為需要清理
|
||||
INSERT INTO migration_log (
|
||||
user_session,
|
||||
migration_type,
|
||||
source_data,
|
||||
success,
|
||||
error_message
|
||||
) VALUES (
|
||||
'system',
|
||||
'image_cleanup',
|
||||
json_build_object('image_path', image_record.image_path),
|
||||
false,
|
||||
'Image marked for cleanup: ' || image_record.image_path
|
||||
);
|
||||
|
||||
deleted_count := deleted_count + 1;
|
||||
END LOOP;
|
||||
|
||||
-- 記錄清理完成
|
||||
INSERT INTO migration_log (
|
||||
user_session,
|
||||
migration_type,
|
||||
target_records,
|
||||
success,
|
||||
error_message
|
||||
) VALUES (
|
||||
'system',
|
||||
'image_cleanup',
|
||||
deleted_count,
|
||||
true,
|
||||
'Orphaned image cleanup completed. Marked ' || deleted_count || ' images for cleanup.'
|
||||
);
|
||||
|
||||
RETURN deleted_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 10. 創建性能檢查函數
|
||||
CREATE OR REPLACE FUNCTION get_performance_stats()
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
result JSON;
|
||||
BEGIN
|
||||
SELECT json_build_object(
|
||||
'table_sizes', (
|
||||
SELECT json_object_agg(
|
||||
table_name,
|
||||
pg_size_pretty(pg_total_relation_size(quote_ident(table_name)))
|
||||
)
|
||||
FROM (
|
||||
SELECT 'wishes' as table_name
|
||||
UNION SELECT 'wish_likes'
|
||||
UNION SELECT 'user_settings'
|
||||
UNION SELECT 'migration_log'
|
||||
UNION SELECT 'system_stats'
|
||||
) tables
|
||||
),
|
||||
'index_usage', (
|
||||
SELECT json_object_agg(
|
||||
indexname,
|
||||
json_build_object(
|
||||
'size', pg_size_pretty(pg_relation_size(indexname::regclass)),
|
||||
'scans', idx_scan,
|
||||
'tuples_read', idx_tup_read,
|
||||
'tuples_fetched', idx_tup_fetch
|
||||
)
|
||||
)
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname = 'public'
|
||||
AND indexname LIKE 'idx_%'
|
||||
),
|
||||
'query_performance', (
|
||||
SELECT json_build_object(
|
||||
'avg_query_time', COALESCE(AVG(mean_exec_time), 0),
|
||||
'total_queries', COALESCE(SUM(calls), 0),
|
||||
'slowest_queries', (
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'query', LEFT(query, 100) || '...',
|
||||
'avg_time', mean_exec_time,
|
||||
'calls', calls
|
||||
)
|
||||
)
|
||||
FROM pg_stat_statements
|
||||
WHERE query LIKE '%wishes%'
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 5
|
||||
)
|
||||
)
|
||||
FROM pg_stat_statements
|
||||
WHERE query LIKE '%wishes%'
|
||||
)
|
||||
) INTO result;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 提交事務
|
||||
COMMIT;
|
||||
|
||||
-- 顯示創建結果
|
||||
DO $$
|
||||
DECLARE
|
||||
view_count INTEGER;
|
||||
function_count INTEGER;
|
||||
BEGIN
|
||||
-- 計算視圖數量
|
||||
SELECT COUNT(*) INTO view_count
|
||||
FROM pg_views
|
||||
WHERE schemaname = 'public';
|
||||
|
||||
-- 計算函數數量
|
||||
SELECT COUNT(*) INTO function_count
|
||||
FROM pg_proc p
|
||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
WHERE n.nspname = 'public'
|
||||
AND p.proname LIKE '%wish%' OR p.proname LIKE 'get_%' OR p.proname LIKE 'cleanup_%';
|
||||
|
||||
RAISE NOTICE '✅ 視圖和函數創建完成!';
|
||||
RAISE NOTICE '📊 創建統計:';
|
||||
RAISE NOTICE ' - 便利視圖:% 個', view_count;
|
||||
RAISE NOTICE ' - 業務函數:% 個', function_count;
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🎯 主要功能:';
|
||||
RAISE NOTICE ' - wishes_with_likes(帶點讚數的困擾案例)';
|
||||
RAISE NOTICE ' - public_wishes(公開困擾案例)';
|
||||
RAISE NOTICE ' - popular_wishes(熱門困擾案例)';
|
||||
RAISE NOTICE ' - search_wishes()(全文搜索)';
|
||||
RAISE NOTICE ' - get_wishes_stats()(統計數據)';
|
||||
RAISE NOTICE ' - cleanup_orphaned_images()(圖片清理)';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🔄 下一步:執行 04-setup-storage.sql';
|
||||
END $$;
|
284
scripts/04-setup-storage.sql
Normal file
284
scripts/04-setup-storage.sql
Normal file
@@ -0,0 +1,284 @@
|
||||
-- 心願星河 - 存儲服務設置
|
||||
-- 執行順序:第 4 步
|
||||
-- 說明:設置 Supabase Storage 桶和相關政策
|
||||
|
||||
-- 注意:此腳本需要在 Supabase Dashboard 的 SQL Editor 中執行
|
||||
-- 某些 Storage 操作可能需要 service_role 權限
|
||||
|
||||
-- 開始事務
|
||||
BEGIN;
|
||||
|
||||
-- 1. 創建主要圖片存儲桶
|
||||
INSERT INTO storage.buckets (
|
||||
id,
|
||||
name,
|
||||
public,
|
||||
file_size_limit,
|
||||
allowed_mime_types,
|
||||
avif_autodetection
|
||||
) VALUES (
|
||||
'wish-images',
|
||||
'wish-images',
|
||||
true,
|
||||
5242880, -- 5MB
|
||||
ARRAY['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'],
|
||||
true
|
||||
) ON CONFLICT (id) DO UPDATE SET
|
||||
file_size_limit = EXCLUDED.file_size_limit,
|
||||
allowed_mime_types = EXCLUDED.allowed_mime_types,
|
||||
avif_autodetection = EXCLUDED.avif_autodetection;
|
||||
|
||||
-- 2. 創建縮圖存儲桶
|
||||
INSERT INTO storage.buckets (
|
||||
id,
|
||||
name,
|
||||
public,
|
||||
file_size_limit,
|
||||
allowed_mime_types,
|
||||
avif_autodetection
|
||||
) VALUES (
|
||||
'wish-thumbnails',
|
||||
'wish-thumbnails',
|
||||
true,
|
||||
1048576, -- 1MB
|
||||
ARRAY['image/jpeg', 'image/jpg', 'image/png', 'image/webp'],
|
||||
true
|
||||
) ON CONFLICT (id) DO UPDATE SET
|
||||
file_size_limit = EXCLUDED.file_size_limit,
|
||||
allowed_mime_types = EXCLUDED.allowed_mime_types,
|
||||
avif_autodetection = EXCLUDED.avif_autodetection;
|
||||
|
||||
-- 3. 創建存儲使用統計表
|
||||
CREATE TABLE IF NOT EXISTS storage_usage (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
bucket_name TEXT NOT NULL,
|
||||
total_files INTEGER DEFAULT 0,
|
||||
total_size_bytes BIGINT DEFAULT 0,
|
||||
last_cleanup_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
UNIQUE(bucket_name)
|
||||
);
|
||||
|
||||
-- 4. 插入初始存儲統計記錄
|
||||
INSERT INTO storage_usage (bucket_name, total_files, total_size_bytes)
|
||||
VALUES
|
||||
('wish-images', 0, 0),
|
||||
('wish-thumbnails', 0, 0)
|
||||
ON CONFLICT (bucket_name) DO NOTHING;
|
||||
|
||||
-- 5. 創建存儲統計更新函數
|
||||
CREATE OR REPLACE FUNCTION update_storage_usage()
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- 更新 wish-images 桶統計
|
||||
INSERT INTO storage_usage (bucket_name, total_files, total_size_bytes, updated_at)
|
||||
SELECT
|
||||
'wish-images',
|
||||
COUNT(*),
|
||||
COALESCE(SUM(metadata->>'size')::BIGINT, 0),
|
||||
NOW()
|
||||
FROM storage.objects
|
||||
WHERE bucket_id = 'wish-images'
|
||||
ON CONFLICT (bucket_name)
|
||||
DO UPDATE SET
|
||||
total_files = EXCLUDED.total_files,
|
||||
total_size_bytes = EXCLUDED.total_size_bytes,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
|
||||
-- 更新 wish-thumbnails 桶統計
|
||||
INSERT INTO storage_usage (bucket_name, total_files, total_size_bytes, updated_at)
|
||||
SELECT
|
||||
'wish-thumbnails',
|
||||
COUNT(*),
|
||||
COALESCE(SUM(metadata->>'size')::BIGINT, 0),
|
||||
NOW()
|
||||
FROM storage.objects
|
||||
WHERE bucket_id = 'wish-thumbnails'
|
||||
ON CONFLICT (bucket_name)
|
||||
DO UPDATE SET
|
||||
total_files = EXCLUDED.total_files,
|
||||
total_size_bytes = EXCLUDED.total_size_bytes,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 6. 創建存儲清理記錄表
|
||||
CREATE TABLE IF NOT EXISTS storage_cleanup_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
bucket_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size BIGINT,
|
||||
cleanup_reason TEXT,
|
||||
cleanup_status TEXT DEFAULT 'pending' CHECK (cleanup_status IN ('pending', 'completed', 'failed')),
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
completed_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- 7. 創建獲取存儲統計的函數
|
||||
CREATE OR REPLACE FUNCTION get_storage_stats()
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
result JSON;
|
||||
BEGIN
|
||||
-- 更新統計數據
|
||||
PERFORM update_storage_usage();
|
||||
|
||||
SELECT json_build_object(
|
||||
'buckets', (
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'name', bucket_name,
|
||||
'total_files', total_files,
|
||||
'total_size_mb', ROUND(total_size_bytes / 1024.0 / 1024.0, 2),
|
||||
'last_updated', updated_at
|
||||
)
|
||||
)
|
||||
FROM storage_usage
|
||||
),
|
||||
'cleanup_pending', (
|
||||
SELECT COUNT(*)
|
||||
FROM storage_cleanup_log
|
||||
WHERE cleanup_status = 'pending'
|
||||
),
|
||||
'total_storage_mb', (
|
||||
SELECT ROUND(SUM(total_size_bytes) / 1024.0 / 1024.0, 2)
|
||||
FROM storage_usage
|
||||
)
|
||||
) INTO result;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 8. 創建標記孤立圖片的函數
|
||||
CREATE OR REPLACE FUNCTION mark_orphaned_images_for_cleanup()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
marked_count INTEGER := 0;
|
||||
image_record RECORD;
|
||||
referenced_images TEXT[];
|
||||
BEGIN
|
||||
-- 獲取所有被引用的圖片路徑
|
||||
SELECT ARRAY_AGG(DISTINCT image_path) INTO referenced_images
|
||||
FROM (
|
||||
SELECT jsonb_array_elements_text(
|
||||
jsonb_path_query_array(images, '$[*].storage_path')
|
||||
) as image_path
|
||||
FROM wishes
|
||||
WHERE status = 'active'
|
||||
AND images IS NOT NULL
|
||||
AND jsonb_array_length(images) > 0
|
||||
) referenced;
|
||||
|
||||
-- 標記孤立的圖片
|
||||
FOR image_record IN
|
||||
SELECT name, metadata->>'size' as file_size
|
||||
FROM storage.objects
|
||||
WHERE bucket_id IN ('wish-images', 'wish-thumbnails')
|
||||
AND (referenced_images IS NULL OR name != ALL(referenced_images))
|
||||
LOOP
|
||||
INSERT INTO storage_cleanup_log (
|
||||
bucket_name,
|
||||
file_path,
|
||||
file_size,
|
||||
cleanup_reason,
|
||||
cleanup_status
|
||||
) VALUES (
|
||||
CASE
|
||||
WHEN image_record.name LIKE '%/thumbnails/%' THEN 'wish-thumbnails'
|
||||
ELSE 'wish-images'
|
||||
END,
|
||||
image_record.name,
|
||||
image_record.file_size::BIGINT,
|
||||
'Orphaned image - not referenced by any active wish',
|
||||
'pending'
|
||||
) ON CONFLICT DO NOTHING;
|
||||
|
||||
marked_count := marked_count + 1;
|
||||
END LOOP;
|
||||
|
||||
-- 記錄清理操作
|
||||
INSERT INTO migration_log (
|
||||
user_session,
|
||||
migration_type,
|
||||
target_records,
|
||||
success,
|
||||
error_message
|
||||
) VALUES (
|
||||
'system',
|
||||
'storage_cleanup',
|
||||
marked_count,
|
||||
true,
|
||||
'Marked ' || marked_count || ' orphaned images for cleanup'
|
||||
);
|
||||
|
||||
RETURN marked_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 9. 創建存儲使用量更新觸發器
|
||||
CREATE OR REPLACE FUNCTION trigger_storage_usage_update()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- 異步更新存儲統計(避免阻塞主要操作)
|
||||
PERFORM pg_notify('storage_usage_update', 'update_needed');
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 10. 為 wishes 表添加存儲使用量更新觸發器
|
||||
DROP TRIGGER IF EXISTS update_storage_on_wish_change ON wishes;
|
||||
CREATE TRIGGER update_storage_on_wish_change
|
||||
AFTER INSERT OR UPDATE OR DELETE ON wishes
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION trigger_storage_usage_update();
|
||||
|
||||
-- 提交事務
|
||||
COMMIT;
|
||||
|
||||
-- 顯示創建結果
|
||||
DO $$
|
||||
DECLARE
|
||||
bucket_count INTEGER;
|
||||
storage_table_count INTEGER;
|
||||
BEGIN
|
||||
-- 計算存儲桶數量
|
||||
SELECT COUNT(*) INTO bucket_count
|
||||
FROM storage.buckets
|
||||
WHERE id LIKE 'wish-%';
|
||||
|
||||
-- 計算存儲相關表格數量
|
||||
SELECT COUNT(*) INTO storage_table_count
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name LIKE '%storage%';
|
||||
|
||||
RAISE NOTICE '✅ 存儲服務設置完成!';
|
||||
RAISE NOTICE '📊 創建統計:';
|
||||
RAISE NOTICE ' - 存儲桶:% 個', bucket_count;
|
||||
RAISE NOTICE ' - 存儲管理表:% 個', storage_table_count;
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🗂️ 存儲桶配置:';
|
||||
RAISE NOTICE ' - wish-images(主圖片,5MB限制)';
|
||||
RAISE NOTICE ' - wish-thumbnails(縮圖,1MB限制)';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🛠️ 管理功能:';
|
||||
RAISE NOTICE ' - 自動統計更新';
|
||||
RAISE NOTICE ' - 孤立圖片檢測';
|
||||
RAISE NOTICE ' - 清理記錄追蹤';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🔄 下一步:執行 05-setup-rls.sql';
|
||||
END $$;
|
||||
|
||||
-- 重要提醒
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '⚠️ 重要提醒:';
|
||||
RAISE NOTICE ' 1. 請確認存儲桶已在 Supabase Dashboard 中顯示';
|
||||
RAISE NOTICE ' 2. 檢查 Storage → Settings 中的政策設置';
|
||||
RAISE NOTICE ' 3. 測試圖片上傳功能是否正常';
|
||||
RAISE NOTICE ' 4. 定期執行 mark_orphaned_images_for_cleanup() 清理孤立圖片';
|
||||
END $$;
|
151
scripts/05-setup-rls.sql
Normal file
151
scripts/05-setup-rls.sql
Normal file
@@ -0,0 +1,151 @@
|
||||
-- 心願星河 - Row Level Security (RLS) 政策設置
|
||||
-- 執行順序:第 5 步(最後一步)
|
||||
-- 說明:設置完整的安全政策,保護數據安全
|
||||
|
||||
-- 開始事務
|
||||
BEGIN;
|
||||
|
||||
-- 1. 啟用所有表格的 RLS
|
||||
ALTER TABLE wishes ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE wish_likes ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE user_settings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE migration_log ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE system_stats ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE storage_usage ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE storage_cleanup_log ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 2. wishes 表格的 RLS 政策
|
||||
|
||||
-- 2.1 查看政策:公開的困擾案例所有人都可以查看
|
||||
DROP POLICY IF EXISTS "Public wishes are viewable by everyone" ON wishes;
|
||||
CREATE POLICY "Public wishes are viewable by everyone" ON wishes
|
||||
FOR SELECT
|
||||
USING (is_public = true AND status = 'active');
|
||||
|
||||
-- 2.2 查看政策:用戶可以查看自己的所有困擾案例
|
||||
DROP POLICY IF EXISTS "Users can view own wishes" ON wishes;
|
||||
CREATE POLICY "Users can view own wishes" ON wishes
|
||||
FOR SELECT
|
||||
USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true));
|
||||
|
||||
-- 2.3 插入政策:所有人都可以提交困擾案例
|
||||
DROP POLICY IF EXISTS "Anyone can insert wishes" ON wishes;
|
||||
CREATE POLICY "Anyone can insert wishes" ON wishes
|
||||
FOR INSERT
|
||||
WITH CHECK (true);
|
||||
|
||||
-- 2.4 更新政策:用戶只能更新自己的困擾案例
|
||||
DROP POLICY IF EXISTS "Users can update own wishes" ON wishes;
|
||||
CREATE POLICY "Users can update own wishes" ON wishes
|
||||
FOR UPDATE
|
||||
USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true))
|
||||
WITH CHECK (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true));
|
||||
|
||||
-- 2.5 刪除政策:用戶只能軟刪除自己的困擾案例
|
||||
DROP POLICY IF EXISTS "Users can delete own wishes" ON wishes;
|
||||
CREATE POLICY "Users can delete own wishes" ON wishes
|
||||
FOR UPDATE
|
||||
USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true))
|
||||
WITH CHECK (status = 'deleted');
|
||||
|
||||
-- 3. wish_likes 表格的 RLS 政策
|
||||
|
||||
-- 3.1 查看政策:所有人都可以查看點讚記錄(用於統計)
|
||||
DROP POLICY IF EXISTS "Wish likes are viewable by everyone" ON wish_likes;
|
||||
CREATE POLICY "Wish likes are viewable by everyone" ON wish_likes
|
||||
FOR SELECT
|
||||
USING (true);
|
||||
|
||||
-- 3.2 插入政策:所有人都可以點讚
|
||||
DROP POLICY IF EXISTS "Anyone can insert wish likes" ON wish_likes;
|
||||
CREATE POLICY "Anyone can insert wish likes" ON wish_likes
|
||||
FOR INSERT
|
||||
WITH CHECK (true);
|
||||
|
||||
-- 3.3 刪除政策:用戶只能取消自己的點讚
|
||||
DROP POLICY IF EXISTS "Users can delete own likes" ON wish_likes;
|
||||
CREATE POLICY "Users can delete own likes" ON wish_likes
|
||||
FOR DELETE
|
||||
USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true));
|
||||
|
||||
-- 4. user_settings 表格的 RLS 政策
|
||||
|
||||
-- 4.1 查看政策:用戶只能查看自己的設定
|
||||
DROP POLICY IF EXISTS "Users can view own settings" ON user_settings;
|
||||
CREATE POLICY "Users can view own settings" ON user_settings
|
||||
FOR SELECT
|
||||
USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true));
|
||||
|
||||
-- 4.2 插入政策:用戶可以創建自己的設定
|
||||
DROP POLICY IF EXISTS "Users can insert own settings" ON user_settings;
|
||||
CREATE POLICY "Users can insert own settings" ON user_settings
|
||||
FOR INSERT
|
||||
WITH CHECK (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true));
|
||||
|
||||
-- 4.3 更新政策:用戶只能更新自己的設定
|
||||
DROP POLICY IF EXISTS "Users can update own settings" ON user_settings;
|
||||
CREATE POLICY "Users can update own settings" ON user_settings
|
||||
FOR UPDATE
|
||||
USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true))
|
||||
WITH CHECK (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true));
|
||||
|
||||
-- 5. migration_log 表格的 RLS 政策
|
||||
|
||||
-- 5.1 查看政策:用戶可以查看自己的遷移記錄
|
||||
DROP POLICY IF EXISTS "Users can view own migration logs" ON migration_log;
|
||||
CREATE POLICY "Users can view own migration logs" ON migration_log
|
||||
FOR SELECT
|
||||
USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR
|
||||
user_session = current_setting('app.user_session', true) OR
|
||||
user_session = 'system');
|
||||
|
||||
-- 5.2 插入政策:系統和用戶都可以插入遷移記錄
|
||||
DROP POLICY IF EXISTS "System and users can insert migration logs" ON migration_log;
|
||||
CREATE POLICY "System and users can insert migration logs" ON migration_log
|
||||
FOR INSERT
|
||||
WITH CHECK (true);
|
||||
|
||||
-- 6. system_stats 表格的 RLS 政策
|
||||
|
||||
-- 6.1 查看政策:所有人都可以查看系統統計(公開數據)
|
||||
DROP POLICY IF EXISTS "System stats are viewable by everyone" ON system_stats;
|
||||
CREATE POLICY "System stats are viewable by everyone" ON system_stats
|
||||
FOR SELECT
|
||||
USING (true);
|
||||
|
||||
-- 6.2 插入/更新政策:只有系統可以修改統計數據
|
||||
DROP POLICY IF EXISTS "Only system can modify stats" ON system_stats;
|
||||
CREATE POLICY "Only system can modify stats" ON system_stats
|
||||
FOR ALL
|
||||
USING (current_user = 'postgres' OR current_setting('role', true) = 'service_role');
|
||||
|
||||
-- 7. storage_usage 表格的 RLS 政策
|
||||
|
||||
-- 7.1 查看政策:所有人都可以查看存儲使用統計
|
||||
DROP POLICY IF EXISTS "Storage usage is viewable by everyone" ON storage_usage;
|
||||
CREATE POLICY "Storage usage is viewable by everyone" ON storage_usage
|
||||
FOR SELECT
|
||||
USING (true);
|
||||
|
||||
-- 7.2 修改政策:只有系統可以修改存儲統計
|
||||
DROP POLICY IF EXISTS "Only system can modify storage usage" ON storage_usage;
|
||||
CREATE POLICY "Only system can modify storage usage" ON storage_usage
|
||||
FOR ALL
|
||||
USING (current_user = 'postgres' OR current_setting('role', true) = 'service_role');
|
||||
|
||||
-- 8. storage_cleanup_log 表格的 RLS 政策
|
||||
|
||||
-- 8.1 查看政策:所有人都可以查看清理記錄
|
||||
DROP POLICY IF EXISTS "Storage cleanup logs are viewable by everyone" ON storage_cleanup_log;
|
||||
CREATE POLICY "Storage cleanup logs are viewable by everyone" ON storage_cleanup_log
|
||||
FOR SELECT
|
||||
USING (true);
|
135
scripts/README-CLEAR-DATA.md
Normal file
135
scripts/README-CLEAR-DATA.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 心願星河 - 數據清空指南
|
||||
|
||||
⚠️ **重要警告**:以下操作將永久刪除所有數據,請謹慎使用!
|
||||
|
||||
## 可用的清空腳本
|
||||
|
||||
### 1. 綜合清空腳本(推薦)
|
||||
```bash
|
||||
node scripts/clear-all.js
|
||||
```
|
||||
**功能**:一次性清空所有數據
|
||||
- 清空 Supabase Storage 中的所有圖片
|
||||
- 清空資料庫中的所有表格
|
||||
- 重置自增序列
|
||||
- 重新插入初始數據
|
||||
- 驗證清空結果
|
||||
|
||||
### 2. 單獨清空 Storage
|
||||
```bash
|
||||
node scripts/clear-storage.js
|
||||
```
|
||||
**功能**:僅清空圖片存儲
|
||||
- 清空 `wish-images` 存儲桶
|
||||
- 清空 `wish-thumbnails` 存儲桶
|
||||
|
||||
### 3. 單獨清空資料庫
|
||||
在 Supabase Dashboard 的 SQL Editor 中執行:
|
||||
```sql
|
||||
-- 執行 scripts/clear-all-data.sql 文件的內容
|
||||
```
|
||||
|
||||
## 使用前準備
|
||||
|
||||
### 1. 確認環境變數
|
||||
確保以下環境變數已正確設置(在 `.env.local` 文件中):
|
||||
```env
|
||||
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
|
||||
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key # 可選,但建議設置
|
||||
```
|
||||
|
||||
### 2. 安裝依賴
|
||||
```bash
|
||||
npm install
|
||||
# 或
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## 使用步驟
|
||||
|
||||
### 方案 A:一鍵清空(推薦)
|
||||
```bash
|
||||
# 執行綜合清空腳本
|
||||
node scripts/clear-all.js
|
||||
|
||||
# 腳本會顯示 10 秒倒計時,按 Ctrl+C 可以取消
|
||||
```
|
||||
|
||||
### 方案 B:分步驟清空
|
||||
```bash
|
||||
# 1. 先清空 Storage
|
||||
node scripts/clear-storage.js
|
||||
|
||||
# 2. 再清空資料庫(在 Supabase Dashboard 中執行)
|
||||
# 將 scripts/clear-all-data.sql 的內容貼到 SQL Editor 中執行
|
||||
```
|
||||
|
||||
## 清空後的檢查
|
||||
|
||||
### 1. 驗證 Storage
|
||||
在 Supabase Dashboard → Storage 中檢查:
|
||||
- `wish-images` 桶應該是空的
|
||||
- `wish-thumbnails` 桶應該是空的
|
||||
|
||||
### 2. 驗證資料庫
|
||||
在 Supabase Dashboard → Table Editor 中檢查:
|
||||
- `wishes` 表應該沒有記錄
|
||||
- `wish_likes` 表應該沒有記錄
|
||||
- `user_settings` 表應該沒有記錄
|
||||
- 其他管理表格會有基礎的初始記錄
|
||||
|
||||
### 3. 測試應用程式
|
||||
```bash
|
||||
# 重新啟動開發服務器
|
||||
npm run dev
|
||||
# 或
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
在瀏覽器中:
|
||||
1. 清除 localStorage(開發者工具 → Application → Local Storage → Clear All)
|
||||
2. 重新載入頁面
|
||||
3. 測試提交新的困擾案例
|
||||
4. 確認功能正常運行
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 權限錯誤
|
||||
```
|
||||
Error: Insufficient permissions
|
||||
```
|
||||
**解決方案**:確保使用 `SUPABASE_SERVICE_ROLE_KEY` 而不是 `ANON_KEY`
|
||||
|
||||
### 2. 存儲桶不存在
|
||||
```
|
||||
Error: Bucket does not exist
|
||||
```
|
||||
**解決方案**:正常現象,腳本會自動跳過不存在的存儲桶
|
||||
|
||||
### 3. 網路錯誤
|
||||
```
|
||||
Error: fetch failed
|
||||
```
|
||||
**解決方案**:檢查網路連接和 Supabase URL 是否正確
|
||||
|
||||
### 4. 資料庫連接錯誤
|
||||
**解決方案**:
|
||||
1. 檢查 Supabase 專案是否暫停
|
||||
2. 驗證 URL 和密鑰是否正確
|
||||
3. 確認專案是否有足夠的配額
|
||||
|
||||
## 注意事項
|
||||
|
||||
1. **備份重要數據**:在生產環境中執行前,請先備份重要數據
|
||||
2. **測試環境優先**:建議先在測試環境中驗證腳本功能
|
||||
3. **瀏覽器清除**:清空數據後記得清除瀏覽器的 localStorage
|
||||
4. **應用重啟**:清空後建議重新啟動應用程式
|
||||
|
||||
## 聯絡支援
|
||||
|
||||
如果遇到問題,請檢查:
|
||||
1. 控制台錯誤訊息
|
||||
2. Supabase Dashboard 中的 Logs
|
||||
3. 網路連接狀態
|
||||
4. 環境變數配置
|
@@ -1,55 +0,0 @@
|
||||
// 清空所有本地存儲資料的腳本
|
||||
// 執行此腳本將清除所有測試數據,讓應用回到初始狀態
|
||||
|
||||
console.log("🧹 開始清空所有測試資料...")
|
||||
|
||||
// 清空的資料項目
|
||||
const dataKeys = [
|
||||
"wishes", // 所有許願/困擾案例
|
||||
"wishLikes", // 點讚數據
|
||||
"userLikedWishes", // 用戶點讚記錄
|
||||
"backgroundMusicState", // 背景音樂狀態
|
||||
]
|
||||
|
||||
let clearedCount = 0
|
||||
|
||||
// 清空每個資料項目
|
||||
dataKeys.forEach((key) => {
|
||||
const existingData = localStorage.getItem(key)
|
||||
if (existingData) {
|
||||
localStorage.removeItem(key)
|
||||
console.log(`✅ 已清空: ${key}`)
|
||||
clearedCount++
|
||||
} else {
|
||||
console.log(`ℹ️ ${key} 已經是空的`)
|
||||
}
|
||||
})
|
||||
|
||||
// 顯示清理結果
|
||||
console.log(`\n🎉 資料清理完成!`)
|
||||
console.log(`📊 清理統計:`)
|
||||
console.log(` - 清空了 ${clearedCount} 個資料項目`)
|
||||
console.log(` - 檢查了 ${dataKeys.length} 個資料項目`)
|
||||
|
||||
// 驗證清理結果
|
||||
console.log(`\n🔍 驗證清理結果:`)
|
||||
dataKeys.forEach((key) => {
|
||||
const data = localStorage.getItem(key)
|
||||
if (data) {
|
||||
console.log(`❌ ${key}: 仍有資料殘留`)
|
||||
} else {
|
||||
console.log(`✅ ${key}: 已完全清空`)
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`\n🚀 應用程式已準備好進行正式佈署!`)
|
||||
console.log(`💡 建議接下來的步驟:`)
|
||||
console.log(` 1. 重新整理頁面確認所有資料已清空`)
|
||||
console.log(` 2. 測試各個功能頁面的初始狀態`)
|
||||
console.log(` 3. 確認沒有錯誤訊息或異常行為`)
|
||||
console.log(` 4. 準備佈署到正式環境`)
|
||||
|
||||
// 提供重新載入頁面的選項
|
||||
if (confirm("是否要重新載入頁面以確認清理效果?")) {
|
||||
window.location.reload()
|
||||
}
|
134
scripts/clear-all-data.sql
Normal file
134
scripts/clear-all-data.sql
Normal file
@@ -0,0 +1,134 @@
|
||||
-- 心願星河 - 清空所有數據
|
||||
-- ⚠️ 警告:此腳本將永久刪除所有數據,請謹慎使用!
|
||||
-- 建議:在生產環境執行前請備份重要數據
|
||||
|
||||
-- 開始事務
|
||||
BEGIN;
|
||||
|
||||
-- 0. 修復 migration_log 表約束問題
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 移除舊的約束
|
||||
ALTER TABLE migration_log DROP CONSTRAINT IF EXISTS migration_log_migration_type_check;
|
||||
|
||||
-- 添加新的約束,包含所有需要的類型
|
||||
ALTER TABLE migration_log ADD CONSTRAINT migration_log_migration_type_check
|
||||
CHECK (migration_type IN ('wishes', 'likes', 'settings', 'storage_cleanup', 'data_cleanup', 'image_cleanup'));
|
||||
|
||||
RAISE NOTICE '🔧 migration_log 表約束已修復';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE NOTICE '⚠️ 修復約束時發生錯誤,但繼續執行: %', SQLERRM;
|
||||
END $$;
|
||||
|
||||
-- 顯示警告訊息
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🚨 準備清空所有數據...';
|
||||
RAISE NOTICE '⚠️ 這將永久刪除:';
|
||||
RAISE NOTICE ' - 所有困擾案例 (wishes)';
|
||||
RAISE NOTICE ' - 所有點讚記錄 (wish_likes)';
|
||||
RAISE NOTICE ' - 所有用戶設定 (user_settings)';
|
||||
RAISE NOTICE ' - 遷移記錄 (migration_log)';
|
||||
RAISE NOTICE ' - 系統統計 (system_stats)';
|
||||
RAISE NOTICE ' - 存儲使用記錄 (storage_usage)';
|
||||
RAISE NOTICE ' - 存儲清理記錄 (storage_cleanup_log)';
|
||||
RAISE NOTICE '';
|
||||
END $$;
|
||||
|
||||
-- 1. 清空所有數據表(按依賴關係順序)
|
||||
DO $$
|
||||
DECLARE
|
||||
table_count INTEGER;
|
||||
BEGIN
|
||||
-- 清空有外鍵關係的表格
|
||||
DELETE FROM wish_likes;
|
||||
GET DIAGNOSTICS table_count = ROW_COUNT;
|
||||
RAISE NOTICE '🗑️ 已清空 wish_likes 表,刪除 % 條記錄', table_count;
|
||||
|
||||
DELETE FROM wishes;
|
||||
GET DIAGNOSTICS table_count = ROW_COUNT;
|
||||
RAISE NOTICE '🗑️ 已清空 wishes 表,刪除 % 條記錄', table_count;
|
||||
|
||||
DELETE FROM user_settings;
|
||||
GET DIAGNOSTICS table_count = ROW_COUNT;
|
||||
RAISE NOTICE '🗑️ 已清空 user_settings 表,刪除 % 條記錄', table_count;
|
||||
|
||||
DELETE FROM migration_log;
|
||||
GET DIAGNOSTICS table_count = ROW_COUNT;
|
||||
RAISE NOTICE '🗑️ 已清空 migration_log 表,刪除 % 條記錄', table_count;
|
||||
|
||||
DELETE FROM system_stats;
|
||||
GET DIAGNOSTICS table_count = ROW_COUNT;
|
||||
RAISE NOTICE '🗑️ 已清空 system_stats 表,刪除 % 條記錄', table_count;
|
||||
|
||||
DELETE FROM storage_usage;
|
||||
GET DIAGNOSTICS table_count = ROW_COUNT;
|
||||
RAISE NOTICE '🗑️ 已清空 storage_usage 表,刪除 % 條記錄', table_count;
|
||||
|
||||
DELETE FROM storage_cleanup_log;
|
||||
GET DIAGNOSTICS table_count = ROW_COUNT;
|
||||
RAISE NOTICE '🗑️ 已清空 storage_cleanup_log 表,刪除 % 條記錄', table_count;
|
||||
END $$;
|
||||
|
||||
-- 2. 重置自增序列
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '🔄 重置自增序列...';
|
||||
|
||||
-- 重置所有表格的序列
|
||||
ALTER SEQUENCE wishes_id_seq RESTART WITH 1;
|
||||
ALTER SEQUENCE wish_likes_id_seq RESTART WITH 1;
|
||||
ALTER SEQUENCE user_settings_id_seq RESTART WITH 1;
|
||||
ALTER SEQUENCE migration_log_id_seq RESTART WITH 1;
|
||||
ALTER SEQUENCE system_stats_id_seq RESTART WITH 1;
|
||||
ALTER SEQUENCE storage_usage_id_seq RESTART WITH 1;
|
||||
ALTER SEQUENCE storage_cleanup_log_id_seq RESTART WITH 1;
|
||||
|
||||
RAISE NOTICE '✅ 所有序列已重置為 1';
|
||||
END $$;
|
||||
|
||||
-- 3. 重新插入初始統計記錄
|
||||
INSERT INTO storage_usage (bucket_name, total_files, total_size_bytes)
|
||||
VALUES
|
||||
('wish-images', 0, 0),
|
||||
('wish-thumbnails', 0, 0);
|
||||
|
||||
INSERT INTO system_stats (stat_date, total_wishes, public_wishes, private_wishes, total_likes, active_users, storage_used_mb)
|
||||
VALUES (CURRENT_DATE, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
-- 4. 記錄清空操作
|
||||
INSERT INTO migration_log (
|
||||
user_session,
|
||||
migration_type,
|
||||
target_records,
|
||||
success,
|
||||
error_message
|
||||
) VALUES (
|
||||
'system-admin',
|
||||
'data_cleanup',
|
||||
0,
|
||||
true,
|
||||
'All data cleared by admin request at ' || NOW()
|
||||
);
|
||||
|
||||
-- 提交事務
|
||||
COMMIT;
|
||||
|
||||
-- 顯示完成訊息
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '✅ 資料庫清空完成!';
|
||||
RAISE NOTICE '📊 重置統計:';
|
||||
RAISE NOTICE ' - 所有表格已清空';
|
||||
RAISE NOTICE ' - 自增序列已重置';
|
||||
RAISE NOTICE ' - 初始統計記錄已重新建立';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '⚠️ 注意:';
|
||||
RAISE NOTICE ' - Storage 中的檔案需要手動清空';
|
||||
RAISE NOTICE ' - 可以使用 clear-storage.js 腳本清空圖片';
|
||||
RAISE NOTICE '';
|
||||
END $$;
|
357
scripts/clear-all.js
Normal file
357
scripts/clear-all.js
Normal file
@@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 心願星河 - 綜合清空腳本
|
||||
*
|
||||
* ⚠️ 警告:此腳本將永久刪除所有數據和文件!
|
||||
*
|
||||
* 功能:
|
||||
* 1. 清空 Supabase Storage 中的所有圖片
|
||||
* 2. 清空資料庫中的所有數據
|
||||
* 3. 重置自增序列
|
||||
* 4. 重新初始化基礎數據
|
||||
*
|
||||
* 使用方法:
|
||||
* node scripts/clear-all.js
|
||||
*/
|
||||
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 載入環境變數
|
||||
require('dotenv').config({ path: '.env.local' });
|
||||
|
||||
// Supabase 配置
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
// 檢查必要的環境變數
|
||||
if (!supabaseUrl || !supabaseServiceKey) {
|
||||
console.error('❌ 錯誤:缺少必要的環境變數');
|
||||
console.error('請確保已設置以下環境變數:');
|
||||
console.error('- NEXT_PUBLIC_SUPABASE_URL');
|
||||
console.error('- SUPABASE_SERVICE_ROLE_KEY 或 NEXT_PUBLIC_SUPABASE_ANON_KEY');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 初始化 Supabase 客戶端
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
/**
|
||||
* 清空存儲桶中的所有文件
|
||||
*/
|
||||
async function clearStorage() {
|
||||
console.log('\n📁 開始清空 Storage...');
|
||||
|
||||
const buckets = ['wish-images', 'wish-thumbnails'];
|
||||
let allSuccess = true;
|
||||
|
||||
for (const bucketName of buckets) {
|
||||
try {
|
||||
console.log(`\n🗂️ 正在處理存儲桶:${bucketName}`);
|
||||
|
||||
// 列出所有文件
|
||||
const { data: files, error: listError } = await supabase.storage
|
||||
.from(bucketName)
|
||||
.list('', { limit: 1000 });
|
||||
|
||||
if (listError) {
|
||||
if (listError.message.includes('not found') || listError.message.includes('does not exist')) {
|
||||
console.log(`⚠️ 存儲桶 ${bucketName} 不存在,跳過`);
|
||||
continue;
|
||||
}
|
||||
console.error(`❌ 列出 ${bucketName} 文件時出錯:`, listError.message);
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
console.log(`✅ ${bucketName} 已經是空的`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 獲取所有文件路徑
|
||||
const allFilePaths = [];
|
||||
for (const file of files) {
|
||||
if (file.name && file.name !== '.emptyFolderPlaceholder') {
|
||||
if (!file.metadata) {
|
||||
// 處理目錄
|
||||
const { data: subFiles } = await supabase.storage
|
||||
.from(bucketName)
|
||||
.list(file.name, { limit: 1000 });
|
||||
|
||||
if (subFiles) {
|
||||
subFiles.forEach(subFile => {
|
||||
if (subFile.name && subFile.name !== '.emptyFolderPlaceholder') {
|
||||
allFilePaths.push(`${file.name}/${subFile.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
allFilePaths.push(file.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allFilePaths.length === 0) {
|
||||
console.log(`✅ ${bucketName} 中沒有需要刪除的文件`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🗑️ 刪除 ${allFilePaths.length} 個文件...`);
|
||||
|
||||
// 批量刪除
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < allFilePaths.length; i += batchSize) {
|
||||
const batch = allFilePaths.slice(i, i + batchSize);
|
||||
const { error } = await supabase.storage.from(bucketName).remove(batch);
|
||||
|
||||
if (error) {
|
||||
console.error(`❌ 刪除批次失敗:`, error.message);
|
||||
allSuccess = false;
|
||||
} else {
|
||||
console.log(`✅ 已刪除 ${Math.min(i + batchSize, allFilePaths.length)}/${allFilePaths.length} 個文件`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 處理 ${bucketName} 時發生錯誤:`, error.message);
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allSuccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修復 migration_log 表的約束問題
|
||||
*/
|
||||
async function fixMigrationLogConstraint() {
|
||||
console.log('\n🔧 修復 migration_log 表約束...');
|
||||
|
||||
try {
|
||||
// 使用 rpc 調用執行 SQL,修復約束
|
||||
const { error } = await supabase.rpc('exec_sql', {
|
||||
sql_query: `
|
||||
ALTER TABLE migration_log DROP CONSTRAINT IF EXISTS migration_log_migration_type_check;
|
||||
ALTER TABLE migration_log ADD CONSTRAINT migration_log_migration_type_check
|
||||
CHECK (migration_type IN ('wishes', 'likes', 'settings', 'storage_cleanup', 'data_cleanup', 'image_cleanup'));
|
||||
`
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log('⚠️ 無法通過 RPC 修復約束,嘗試其他方法...');
|
||||
// 如果 RPC 方法失敗,我們繼續執行,但會在日誌中使用允許的類型
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('✅ migration_log 表約束已修復');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('⚠️ 修復約束時發生錯誤,但繼續執行:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空資料庫數據
|
||||
*/
|
||||
async function clearDatabase() {
|
||||
console.log('\n🗄️ 開始清空資料庫...');
|
||||
|
||||
try {
|
||||
// 1. 清空有外鍵關係的表格(按依賴順序)
|
||||
const tablesToClear = [
|
||||
{ name: 'wish_likes', description: '點讚記錄' },
|
||||
{ name: 'wishes', description: '困擾案例' },
|
||||
{ name: 'user_settings', description: '用戶設定' },
|
||||
{ name: 'system_stats', description: '系統統計' },
|
||||
{ name: 'storage_usage', description: '存儲使用記錄' },
|
||||
{ name: 'storage_cleanup_log', description: '存儲清理記錄' },
|
||||
{ name: 'migration_log', description: '遷移記錄' }
|
||||
];
|
||||
|
||||
for (const table of tablesToClear) {
|
||||
try {
|
||||
const { error } = await supabase.from(table.name).delete().neq('id', 0);
|
||||
if (error) {
|
||||
console.error(`❌ 清空 ${table.name} (${table.description}) 表失敗:`, error.message);
|
||||
// 如果不是 migration_log 表,則返回失敗
|
||||
if (table.name !== 'migration_log') {
|
||||
return false;
|
||||
}
|
||||
// migration_log 表清空失敗可以忽略,因為我們稍後會重新插入
|
||||
console.log(`⚠️ ${table.name} 表清空失敗,將在後續步驟中處理`);
|
||||
} else {
|
||||
console.log(`✅ 已清空 ${table.name} (${table.description}) 表`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`❌ 處理 ${table.name} 表時發生錯誤:`, err.message);
|
||||
if (table.name !== 'migration_log') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 重新插入初始數據
|
||||
console.log('\n🔧 重新插入初始數據...');
|
||||
|
||||
// 插入初始存儲統計
|
||||
await supabase.from('storage_usage').insert([
|
||||
{ bucket_name: 'wish-images', total_files: 0, total_size_bytes: 0 },
|
||||
{ bucket_name: 'wish-thumbnails', total_files: 0, total_size_bytes: 0 }
|
||||
]);
|
||||
|
||||
// 插入今日初始統計
|
||||
await supabase.from('system_stats').insert([{
|
||||
stat_date: new Date().toISOString().split('T')[0],
|
||||
total_wishes: 0,
|
||||
public_wishes: 0,
|
||||
private_wishes: 0,
|
||||
total_likes: 0,
|
||||
active_users: 0,
|
||||
storage_used_mb: 0
|
||||
}]);
|
||||
|
||||
// 記錄清空操作(最後執行,避免約束衝突)
|
||||
try {
|
||||
await supabase.from('migration_log').insert([{
|
||||
user_session: 'system-admin',
|
||||
migration_type: 'data_cleanup',
|
||||
target_records: 0,
|
||||
success: true,
|
||||
error_message: `All data cleared by admin request at ${new Date().toISOString()}`
|
||||
}]);
|
||||
console.log('✅ 清空操作記錄已插入');
|
||||
} catch (logError) {
|
||||
console.log('⚠️ 無法插入清空操作記錄,但不影響清空結果:', logError.message);
|
||||
}
|
||||
|
||||
console.log('✅ 初始數據插入完成');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 清空資料庫時發生錯誤:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證清空結果
|
||||
*/
|
||||
async function verifyCleanup() {
|
||||
console.log('\n🔍 驗證清空結果...');
|
||||
|
||||
try {
|
||||
// 檢查主要數據表
|
||||
const { data: wishes, error: wishesError } = await supabase
|
||||
.from('wishes')
|
||||
.select('count', { count: 'exact', head: true });
|
||||
|
||||
const { data: likes, error: likesError } = await supabase
|
||||
.from('wish_likes')
|
||||
.select('count', { count: 'exact', head: true });
|
||||
|
||||
if (wishesError || likesError) {
|
||||
console.error('❌ 驗證時發生錯誤');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`📊 驗證結果:`);
|
||||
console.log(` - wishes 表:${wishes || 0} 條記錄`);
|
||||
console.log(` - wish_likes 表:${likes || 0} 條記錄`);
|
||||
|
||||
// 檢查存儲
|
||||
const buckets = ['wish-images', 'wish-thumbnails'];
|
||||
for (const bucket of buckets) {
|
||||
const { data: files } = await supabase.storage.from(bucket).list('', { limit: 1 });
|
||||
console.log(` - ${bucket} 存儲桶:${files ? files.length : 0} 個文件`);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 驗證時發生錯誤:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函數
|
||||
*/
|
||||
async function main() {
|
||||
console.log('🚀 心願星河 - 綜合數據清空');
|
||||
console.log('⚠️ 警告:這將永久刪除所有數據和文件!');
|
||||
console.log('\n包含:');
|
||||
console.log('- 所有困擾案例和點讚記錄');
|
||||
console.log('- 所有用戶設定');
|
||||
console.log('- Storage 中的所有圖片文件');
|
||||
console.log('- 系統統計和記錄');
|
||||
|
||||
// 給用戶考慮時間
|
||||
console.log('\n⏰ 10 秒後開始清空... (按 Ctrl+C 取消)');
|
||||
for (let i = 10; i > 0; i--) {
|
||||
process.stdout.write(`\r倒計時:${i} 秒 `);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
console.log('\n\n開始執行清空操作...\n');
|
||||
|
||||
let success = true;
|
||||
|
||||
// 0. 修復約束問題
|
||||
const constraintFixed = await fixMigrationLogConstraint();
|
||||
|
||||
// 1. 清空存儲
|
||||
const storageSuccess = await clearStorage();
|
||||
if (!storageSuccess) {
|
||||
console.log('⚠️ Storage 清空過程中有錯誤,但繼續執行資料庫清空');
|
||||
}
|
||||
|
||||
// 2. 清空資料庫
|
||||
const dbSuccess = await clearDatabase();
|
||||
if (!dbSuccess) {
|
||||
console.error('❌ 資料庫清空失敗');
|
||||
success = false;
|
||||
}
|
||||
|
||||
// 3. 驗證結果
|
||||
if (success) {
|
||||
await verifyCleanup();
|
||||
}
|
||||
|
||||
// 顯示最終結果
|
||||
console.log('\n' + '='.repeat(60));
|
||||
if (success) {
|
||||
console.log('✅ 所有數據清空完成!');
|
||||
console.log('\n📝 建議後續步驟:');
|
||||
console.log('1. 重新啟動應用程式');
|
||||
console.log('2. 在瀏覽器中清除 localStorage');
|
||||
console.log('3. 確認應用程式正常運行');
|
||||
} else {
|
||||
console.log('❌ 清空過程中有錯誤,請檢查上述訊息');
|
||||
}
|
||||
|
||||
process.exit(success ? 0 : 1);
|
||||
}
|
||||
|
||||
// 錯誤處理
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.error('❌ 未處理的錯誤:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n❌ 用戶取消操作');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 執行主函數
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('❌ 腳本執行失敗:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
221
scripts/clear-storage.js
Normal file
221
scripts/clear-storage.js
Normal file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 心願星河 - 清空 Supabase Storage
|
||||
*
|
||||
* ⚠️ 警告:此腳本將永久刪除所有存儲的圖片文件!
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 確保已安裝依賴:npm install
|
||||
* 2. 設置環境變數或在 .env.local 中配置 Supabase 連接
|
||||
* 3. 執行腳本:node scripts/clear-storage.js
|
||||
*/
|
||||
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 載入環境變數
|
||||
require('dotenv').config({ path: '.env.local' });
|
||||
|
||||
// Supabase 配置
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
// 檢查必要的環境變數
|
||||
if (!supabaseUrl || !supabaseServiceKey) {
|
||||
console.error('❌ 錯誤:缺少必要的環境變數');
|
||||
console.error('請確保已設置以下環境變數:');
|
||||
console.error('- NEXT_PUBLIC_SUPABASE_URL');
|
||||
console.error('- SUPABASE_SERVICE_ROLE_KEY 或 NEXT_PUBLIC_SUPABASE_ANON_KEY');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 初始化 Supabase 客戶端
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// 存儲桶名稱
|
||||
const BUCKETS = ['wish-images', 'wish-thumbnails'];
|
||||
|
||||
/**
|
||||
* 清空指定存儲桶中的所有文件
|
||||
*/
|
||||
async function clearBucket(bucketName) {
|
||||
try {
|
||||
console.log(`\n🗂️ 正在處理存儲桶:${bucketName}`);
|
||||
|
||||
// 列出所有文件
|
||||
const { data: files, error: listError } = await supabase.storage
|
||||
.from(bucketName)
|
||||
.list('', {
|
||||
limit: 1000,
|
||||
sortBy: { column: 'created_at', order: 'desc' }
|
||||
});
|
||||
|
||||
if (listError) {
|
||||
console.error(`❌ 列出 ${bucketName} 文件時出錯:`, listError.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
console.log(`✅ ${bucketName} 已經是空的`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`📊 找到 ${files.length} 個文件`);
|
||||
|
||||
// 獲取所有文件路徑(包括子目錄)
|
||||
const allFilePaths = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (file.name && file.name !== '.emptyFolderPlaceholder') {
|
||||
// 如果是目錄,遞歸獲取其中的文件
|
||||
if (!file.metadata) {
|
||||
const { data: subFiles, error: subListError } = await supabase.storage
|
||||
.from(bucketName)
|
||||
.list(file.name, { limit: 1000 });
|
||||
|
||||
if (!subListError && subFiles) {
|
||||
subFiles.forEach(subFile => {
|
||||
if (subFile.name && subFile.name !== '.emptyFolderPlaceholder') {
|
||||
allFilePaths.push(`${file.name}/${subFile.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
allFilePaths.push(file.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allFilePaths.length === 0) {
|
||||
console.log(`✅ ${bucketName} 中沒有需要刪除的文件`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`🗑️ 準備刪除 ${allFilePaths.length} 個文件...`);
|
||||
|
||||
// 批量刪除文件
|
||||
const batchSize = 50; // Supabase 建議的批量操作大小
|
||||
let totalDeleted = 0;
|
||||
let hasErrors = false;
|
||||
|
||||
for (let i = 0; i < allFilePaths.length; i += batchSize) {
|
||||
const batch = allFilePaths.slice(i, i + batchSize);
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucketName)
|
||||
.remove(batch);
|
||||
|
||||
if (error) {
|
||||
console.error(`❌ 刪除批次 ${Math.floor(i/batchSize) + 1} 時出錯:`, error.message);
|
||||
hasErrors = true;
|
||||
} else {
|
||||
totalDeleted += batch.length;
|
||||
console.log(`✅ 已刪除批次 ${Math.floor(i/batchSize) + 1}/${Math.ceil(allFilePaths.length/batchSize)} (${batch.length} 個文件)`);
|
||||
}
|
||||
|
||||
// 避免請求過於頻繁
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
console.log(`📊 ${bucketName} 清空完成:刪除了 ${totalDeleted}/${allFilePaths.length} 個文件`);
|
||||
return !hasErrors;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 清空 ${bucketName} 時發生未預期錯誤:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證存儲桶是否存在
|
||||
*/
|
||||
async function verifyBuckets() {
|
||||
try {
|
||||
const { data: buckets, error } = await supabase.storage.listBuckets();
|
||||
|
||||
if (error) {
|
||||
console.error('❌ 無法獲取存儲桶列表:', error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingBuckets = buckets.map(bucket => bucket.id);
|
||||
const missingBuckets = BUCKETS.filter(bucket => !existingBuckets.includes(bucket));
|
||||
|
||||
if (missingBuckets.length > 0) {
|
||||
console.warn('⚠️ 以下存儲桶不存在,將跳過:', missingBuckets.join(', '));
|
||||
return BUCKETS.filter(bucket => existingBuckets.includes(bucket));
|
||||
}
|
||||
|
||||
return BUCKETS;
|
||||
} catch (error) {
|
||||
console.error('❌ 驗證存儲桶時發生錯誤:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函數
|
||||
*/
|
||||
async function main() {
|
||||
console.log('🚀 開始清空 Supabase Storage...');
|
||||
console.log('⚠️ 警告:這將永久刪除所有存儲的圖片文件!');
|
||||
|
||||
// 驗證存儲桶
|
||||
const bucketsToProcess = await verifyBuckets();
|
||||
if (!bucketsToProcess || bucketsToProcess.length === 0) {
|
||||
console.error('❌ 沒有可處理的存儲桶');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📋 將處理 ${bucketsToProcess.length} 個存儲桶:`, bucketsToProcess.join(', '));
|
||||
|
||||
// 給用戶 5 秒鐘考慮時間
|
||||
console.log('\n⏰ 5 秒後開始刪除... (按 Ctrl+C 取消)');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
let allSuccess = true;
|
||||
|
||||
// 清空每個存儲桶
|
||||
for (const bucket of bucketsToProcess) {
|
||||
const success = await clearBucket(bucket);
|
||||
if (!success) {
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 顯示最終結果
|
||||
console.log('\n' + '='.repeat(50));
|
||||
if (allSuccess) {
|
||||
console.log('✅ 所有存儲桶清空完成!');
|
||||
} else {
|
||||
console.log('⚠️ 存儲桶清空完成,但過程中有一些錯誤');
|
||||
}
|
||||
|
||||
console.log('\n📝 建議後續步驟:');
|
||||
console.log('1. 在 Supabase Dashboard 中確認 Storage 已清空');
|
||||
console.log('2. 執行 clear-all-data.sql 清空資料庫');
|
||||
console.log('3. 重新啟動應用程式');
|
||||
}
|
||||
|
||||
// 錯誤處理
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.error('❌ 未處理的錯誤:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n❌ 用戶取消操作');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 執行主函數
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('❌ 腳本執行失敗:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { clearBucket, verifyBuckets };
|
26
scripts/fix-migration-log-constraint.sql
Normal file
26
scripts/fix-migration-log-constraint.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- 修復 migration_log 表的約束問題
|
||||
-- 允許 'storage_cleanup' 和 'data_cleanup' 類型
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 移除舊的約束
|
||||
ALTER TABLE migration_log DROP CONSTRAINT IF EXISTS migration_log_migration_type_check;
|
||||
|
||||
-- 添加新的約束,包含所有需要的類型
|
||||
ALTER TABLE migration_log ADD CONSTRAINT migration_log_migration_type_check
|
||||
CHECK (migration_type IN ('wishes', 'likes', 'settings', 'storage_cleanup', 'data_cleanup', 'image_cleanup'));
|
||||
|
||||
-- 顯示結果
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '✅ migration_log 表約束已更新';
|
||||
RAISE NOTICE '📋 允許的 migration_type 值:';
|
||||
RAISE NOTICE ' - wishes(困擾案例遷移)';
|
||||
RAISE NOTICE ' - likes(點讚記錄遷移)';
|
||||
RAISE NOTICE ' - settings(用戶設定遷移)';
|
||||
RAISE NOTICE ' - storage_cleanup(存儲清理)';
|
||||
RAISE NOTICE ' - data_cleanup(數據清空)';
|
||||
RAISE NOTICE ' - image_cleanup(圖片清理)';
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
139
scripts/production-cleanup.js
Normal file
139
scripts/production-cleanup.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// 正式環境佈署前的完整資料清理腳本
|
||||
// 執行此腳本將清除所有測試資料,重置到正式環境狀態
|
||||
|
||||
console.log("🚀 開始準備正式環境佈署...")
|
||||
console.log("=".repeat(50))
|
||||
|
||||
// 1. 清空所有本地存儲資料
|
||||
console.log("📋 第一步:清理本地存儲資料")
|
||||
|
||||
const dataKeys = [
|
||||
"wishes", // 所有許願/困擾案例
|
||||
"wishLikes", // 點讚數據
|
||||
"userLikedWishes", // 用戶點讚記錄
|
||||
"backgroundMusicState", // 背景音樂狀態
|
||||
]
|
||||
|
||||
let clearedCount = 0
|
||||
let totalDataSize = 0
|
||||
|
||||
// 計算清理前的資料大小
|
||||
dataKeys.forEach((key) => {
|
||||
const data = localStorage.getItem(key)
|
||||
if (data) {
|
||||
totalDataSize += data.length
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`📊 清理前資料統計:`)
|
||||
console.log(` - 總資料大小: ${(totalDataSize / 1024).toFixed(2)} KB`)
|
||||
|
||||
// 清空每個資料項目
|
||||
dataKeys.forEach((key) => {
|
||||
const existingData = localStorage.getItem(key)
|
||||
if (existingData) {
|
||||
const dataSize = existingData.length
|
||||
localStorage.removeItem(key)
|
||||
console.log(`✅ 已清空: ${key} (${(dataSize / 1024).toFixed(2)} KB)`)
|
||||
clearedCount++
|
||||
} else {
|
||||
console.log(`ℹ️ ${key} 已經是空的`)
|
||||
}
|
||||
})
|
||||
|
||||
console.log("\n" + "=".repeat(50))
|
||||
|
||||
// 2. 設定正式環境的初始狀態
|
||||
console.log("⚙️ 第二步:設定正式環境初始狀態")
|
||||
|
||||
const productionDefaults = {
|
||||
wishes: [],
|
||||
wishLikes: {},
|
||||
userLikedWishes: [],
|
||||
backgroundMusicState: {
|
||||
enabled: false,
|
||||
volume: 0.3,
|
||||
isPlaying: false,
|
||||
},
|
||||
}
|
||||
|
||||
// 設定初始狀態
|
||||
Object.entries(productionDefaults).forEach(([key, value]) => {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
console.log(`✅ 已設定: ${key} 初始狀態`)
|
||||
})
|
||||
|
||||
console.log("\n" + "=".repeat(50))
|
||||
|
||||
// 3. 驗證清理結果
|
||||
console.log("🔍 第三步:驗證清理結果")
|
||||
|
||||
let verificationPassed = true
|
||||
|
||||
dataKeys.forEach((key) => {
|
||||
const data = localStorage.getItem(key)
|
||||
if (data) {
|
||||
const parsedData = JSON.parse(data)
|
||||
|
||||
// 檢查是否為空狀態
|
||||
if (key === "wishes" && Array.isArray(parsedData) && parsedData.length === 0) {
|
||||
console.log(`✅ ${key}: 已重置為空陣列`)
|
||||
} else if (
|
||||
(key === "wishLikes" || key === "userLikedWishes") &&
|
||||
((Array.isArray(parsedData) && parsedData.length === 0) ||
|
||||
(typeof parsedData === "object" && Object.keys(parsedData).length === 0))
|
||||
) {
|
||||
console.log(`✅ ${key}: 已重置為空狀態`)
|
||||
} else if (key === "backgroundMusicState" && typeof parsedData === "object") {
|
||||
console.log(`✅ ${key}: 已重置為預設狀態`)
|
||||
} else {
|
||||
console.log(`❌ ${key}: 狀態異常`)
|
||||
verificationPassed = false
|
||||
}
|
||||
} else {
|
||||
console.log(`❌ ${key}: 資料遺失`)
|
||||
verificationPassed = false
|
||||
}
|
||||
})
|
||||
|
||||
console.log("\n" + "=".repeat(50))
|
||||
|
||||
// 4. 顯示最終結果
|
||||
console.log("🎉 清理完成報告:")
|
||||
console.log(`📊 清理統計:`)
|
||||
console.log(` - 清空了 ${clearedCount} 個資料項目`)
|
||||
console.log(` - 檢查了 ${dataKeys.length} 個資料項目`)
|
||||
console.log(` - 釋放了 ${(totalDataSize / 1024).toFixed(2)} KB 空間`)
|
||||
console.log(` - 驗證結果: ${verificationPassed ? "✅ 通過" : "❌ 失敗"}`)
|
||||
|
||||
console.log("\n🚀 正式環境準備狀態:")
|
||||
console.log(" ✅ 困擾案例: 0 個")
|
||||
console.log(" ✅ 點讚記錄: 已清空")
|
||||
console.log(" ✅ 背景音樂: 預設關閉")
|
||||
console.log(" ✅ 本地存儲: 已重置")
|
||||
|
||||
console.log("\n" + "=".repeat(50))
|
||||
|
||||
if (verificationPassed) {
|
||||
console.log("🎯 佈署準備完成!")
|
||||
console.log("✨ 應用程式已準備好進行正式佈署")
|
||||
console.log("\n📋 建議的佈署檢查清單:")
|
||||
console.log(" □ 重新整理頁面確認所有資料已清空")
|
||||
console.log(" □ 測試各個功能頁面的初始狀態")
|
||||
console.log(" □ 確認沒有錯誤訊息或異常行為")
|
||||
console.log(" □ 檢查響應式設計在各裝置正常")
|
||||
console.log(" □ 測試音效和背景音樂功能")
|
||||
console.log(" □ 驗證隱私設定功能")
|
||||
console.log(" □ 準備佈署到正式環境")
|
||||
|
||||
// 提供重新載入頁面的選項
|
||||
setTimeout(() => {
|
||||
if (confirm("✅ 清理完成!是否要重新載入頁面以確認效果?")) {
|
||||
window.location.reload()
|
||||
}
|
||||
}, 2000)
|
||||
} else {
|
||||
console.log("⚠️ 清理過程中發現問題,請檢查後重新執行")
|
||||
}
|
||||
|
||||
console.log("\n🌟 感謝使用心願星河!準備為用戶提供優質服務!")
|
@@ -1,42 +0,0 @@
|
||||
// 重置應用到正式環境狀態
|
||||
// 這個腳本會清空所有測試資料並設定適合正式環境的初始狀態
|
||||
|
||||
console.log("🔄 正在重置應用到正式環境狀態...")
|
||||
|
||||
// 1. 清空所有本地存儲資料
|
||||
const dataKeys = ["wishes", "wishLikes", "userLikedWishes", "backgroundMusicState"]
|
||||
|
||||
dataKeys.forEach((key) => {
|
||||
localStorage.removeItem(key)
|
||||
})
|
||||
|
||||
// 2. 設定正式環境的初始狀態
|
||||
const productionDefaults = {
|
||||
wishes: [],
|
||||
wishLikes: {},
|
||||
userLikedWishes: [],
|
||||
backgroundMusicState: {
|
||||
enabled: false,
|
||||
volume: 0.3,
|
||||
isPlaying: false,
|
||||
},
|
||||
}
|
||||
|
||||
// 設定初始狀態
|
||||
Object.entries(productionDefaults).forEach(([key, value]) => {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
})
|
||||
|
||||
console.log("✅ 應用已重置到正式環境狀態")
|
||||
console.log("📋 初始狀態設定:")
|
||||
console.log(" - 困擾案例: 0 個")
|
||||
console.log(" - 點讚記錄: 已清空")
|
||||
console.log(" - 背景音樂: 預設關閉")
|
||||
|
||||
console.log("\n🎯 正式環境準備完成!")
|
||||
console.log("🚀 可以開始佈署了")
|
||||
|
||||
// 重新載入頁面
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 2000)
|
137
scripts/test-supabase-connection.js
Normal file
137
scripts/test-supabase-connection.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// 心願星河 - Supabase 連接測試腳本
|
||||
// 使用方法: npm run test-supabase
|
||||
|
||||
const { createClient } = require("@supabase/supabase-js")
|
||||
require("dotenv").config({ path: ".env.local" })
|
||||
|
||||
async function testSupabaseConnection() {
|
||||
console.log("🔍 測試 Supabase 連接...\n")
|
||||
|
||||
// 檢查環境變數
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
|
||||
if (!supabaseUrl || !supabaseKey) {
|
||||
console.error("❌ 環境變數未設置")
|
||||
console.log("請確認 .env.local 檔案中包含:")
|
||||
console.log("- NEXT_PUBLIC_SUPABASE_URL")
|
||||
console.log("- NEXT_PUBLIC_SUPABASE_ANON_KEY")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("✅ 環境變數已設置")
|
||||
console.log(`📍 Supabase URL: ${supabaseUrl}`)
|
||||
console.log(`🔑 API Key: ${supabaseKey.substring(0, 20)}...`)
|
||||
|
||||
// 創建 Supabase 客戶端
|
||||
const supabase = createClient(supabaseUrl, supabaseKey)
|
||||
|
||||
try {
|
||||
// 測試基本連接
|
||||
console.log("\n🔗 測試基本連接...")
|
||||
const { data, error } = await supabase.from("wishes").select("count").limit(1)
|
||||
|
||||
if (error) {
|
||||
console.error("❌ 連接失敗:", error.message)
|
||||
return false
|
||||
}
|
||||
|
||||
console.log("✅ 基本連接成功")
|
||||
|
||||
// 測試表格存在性
|
||||
console.log("\n📊 檢查表格結構...")
|
||||
const tables = ["wishes", "wish_likes", "user_settings", "migration_log", "system_stats"]
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const { data, error } = await supabase.from(table).select("*").limit(1)
|
||||
|
||||
if (error) {
|
||||
console.log(`❌ 表格 ${table}: ${error.message}`)
|
||||
} else {
|
||||
console.log(`✅ 表格 ${table}: 正常`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`❌ 表格 ${table}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 測試視圖
|
||||
console.log("\n👁️ 檢查視圖...")
|
||||
const views = ["wishes_with_likes", "public_wishes", "popular_wishes"]
|
||||
|
||||
for (const view of views) {
|
||||
try {
|
||||
const { data, error } = await supabase.from(view).select("*").limit(1)
|
||||
|
||||
if (error) {
|
||||
console.log(`❌ 視圖 ${view}: ${error.message}`)
|
||||
} else {
|
||||
console.log(`✅ 視圖 ${view}: 正常`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`❌ 視圖 ${view}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 測試函數
|
||||
console.log("\n⚙️ 測試函數...")
|
||||
try {
|
||||
const { data, error } = await supabase.rpc("get_wishes_stats")
|
||||
|
||||
if (error) {
|
||||
console.log(`❌ 函數 get_wishes_stats: ${error.message}`)
|
||||
} else {
|
||||
console.log("✅ 函數 get_wishes_stats: 正常")
|
||||
console.log("📈 統計數據:", JSON.stringify(data, null, 2))
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`❌ 函數測試失敗: ${err.message}`)
|
||||
}
|
||||
|
||||
// 測試存儲
|
||||
console.log("\n🗂️ 檢查存儲桶...")
|
||||
try {
|
||||
const { data: buckets, error } = await supabase.storage.listBuckets()
|
||||
|
||||
if (error) {
|
||||
console.log(`❌ 存儲桶檢查失敗: ${error.message}`)
|
||||
} else {
|
||||
const wishBuckets = buckets.filter((bucket) => bucket.id === "wish-images" || bucket.id === "wish-thumbnails")
|
||||
|
||||
if (wishBuckets.length === 2) {
|
||||
console.log("✅ 存儲桶設置完成")
|
||||
wishBuckets.forEach((bucket) => {
|
||||
console.log(` - ${bucket.id}: ${bucket.public ? "公開" : "私密"}`)
|
||||
})
|
||||
} else {
|
||||
console.log(`⚠️ 存儲桶不完整,找到 ${wishBuckets.length}/2 個`)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`❌ 存儲桶檢查失敗: ${err.message}`)
|
||||
}
|
||||
|
||||
console.log("\n🎉 Supabase 連接測試完成!")
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("❌ 測試過程中發生錯誤:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 執行測試
|
||||
testSupabaseConnection()
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
console.log("\n✅ 所有測試通過,可以開始使用 Supabase!")
|
||||
process.exit(0)
|
||||
} else {
|
||||
console.log("\n❌ 測試失敗,請檢查配置")
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("測試腳本執行失敗:", error)
|
||||
process.exit(1)
|
||||
})
|
Reference in New Issue
Block a user