新增資料庫架構

This commit is contained in:
2025-07-19 02:12:37 +08:00
parent e3832acfa8
commit 924f03c3d7
45 changed files with 12858 additions and 324 deletions

View 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-55最高';
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 $$;

View 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 $$;

View 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 $$;

View 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
View 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);

View 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. 環境變數配置

View File

@@ -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
View 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
View 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
View 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 };

View 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;

View 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🌟 感謝使用心願星河!準備為用戶提供優質服務!")

View File

@@ -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)

View 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)
})