新增資料庫架構

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

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