2026-01-15 13:15:18 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
use App\Modules\Core\Models\Tenant;
|
2026-01-15 13:15:18 +08:00
|
|
|
use Illuminate\Console\Command;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 將現有資料遷移到租戶資料庫
|
|
|
|
|
*
|
|
|
|
|
* 此指令用於初次設定多租戶時,將現有的 ERP 資料遷移到第一個租戶
|
|
|
|
|
*/
|
|
|
|
|
class MigrateToTenant extends Command
|
|
|
|
|
{
|
|
|
|
|
protected $signature = 'tenancy:migrate-data {tenant_id} {--dry-run : 只顯示會遷移的表,不實際執行}';
|
|
|
|
|
protected $description = '將現有 central DB 資料遷移到指定租戶資料庫';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 需要遷移的表 (依賴順序排列)
|
|
|
|
|
*/
|
|
|
|
|
protected array $tablesToMigrate = [
|
|
|
|
|
'users',
|
|
|
|
|
'password_reset_tokens',
|
|
|
|
|
'sessions',
|
|
|
|
|
'cache',
|
|
|
|
|
'cache_locks',
|
|
|
|
|
'jobs',
|
|
|
|
|
'job_batches',
|
|
|
|
|
'failed_jobs',
|
|
|
|
|
'categories',
|
|
|
|
|
'units',
|
|
|
|
|
'vendors',
|
|
|
|
|
'products',
|
|
|
|
|
'product_vendor',
|
|
|
|
|
'warehouses',
|
|
|
|
|
'inventories',
|
|
|
|
|
'inventory_transactions',
|
|
|
|
|
'purchase_orders',
|
|
|
|
|
'purchase_order_items',
|
|
|
|
|
'permissions',
|
|
|
|
|
'roles',
|
|
|
|
|
'model_has_permissions',
|
|
|
|
|
'model_has_roles',
|
|
|
|
|
'role_has_permissions',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public function handle(): int
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->argument('tenant_id');
|
|
|
|
|
$dryRun = $this->option('dry-run');
|
|
|
|
|
|
|
|
|
|
// 檢查租戶是否存在
|
|
|
|
|
$tenant = Tenant::find($tenantId);
|
|
|
|
|
if (!$tenant) {
|
|
|
|
|
$this->error("租戶 '{$tenantId}' 不存在!請先建立租戶。");
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->info("開始遷移資料到租戶: {$tenantId}");
|
|
|
|
|
$this->info("租戶資料庫: tenant{$tenantId}");
|
|
|
|
|
|
|
|
|
|
if ($dryRun) {
|
|
|
|
|
$this->warn('⚠️ Dry Run 模式 - 不會實際遷移資料');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 取得 central 資料庫連線
|
|
|
|
|
$centralConnection = config('database.default');
|
|
|
|
|
$tenantDbName = 'tenant' . $tenantId;
|
|
|
|
|
|
|
|
|
|
foreach ($this->tablesToMigrate as $table) {
|
|
|
|
|
// 檢查表是否存在於 central
|
|
|
|
|
if (!Schema::connection($centralConnection)->hasTable($table)) {
|
|
|
|
|
$this->line(" ⏭️ 跳過 {$table} (表不存在)");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 計算資料筆數
|
|
|
|
|
$count = DB::connection($centralConnection)->table($table)->count();
|
|
|
|
|
if ($count === 0) {
|
|
|
|
|
$this->line(" ⏭️ 跳過 {$table} (無資料)");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($dryRun) {
|
|
|
|
|
$this->info(" 📋 {$table}: {$count} 筆資料");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 實際遷移資料
|
|
|
|
|
$this->info(" 🔄 遷移 {$table}: {$count} 筆資料...");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 使用租戶上下文執行
|
|
|
|
|
$tenant->run(function () use ($centralConnection, $table) {
|
|
|
|
|
// 取得 central 資料
|
|
|
|
|
$data = DB::connection($centralConnection)->table($table)->get();
|
|
|
|
|
|
|
|
|
|
if ($data->isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 關閉外鍵檢查
|
|
|
|
|
DB::statement('SET FOREIGN_KEY_CHECKS=0');
|
|
|
|
|
|
|
|
|
|
// 清空目標表
|
|
|
|
|
DB::table($table)->truncate();
|
|
|
|
|
|
|
|
|
|
// 分批插入 (每批 100 筆)
|
|
|
|
|
foreach ($data->chunk(100) as $chunk) {
|
|
|
|
|
DB::table($table)->insert($chunk->map(fn($item) => (array) $item)->toArray());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 恢復外鍵檢查
|
|
|
|
|
DB::statement('SET FOREIGN_KEY_CHECKS=1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$this->info(" ✅ {$table} 遷移完成");
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
$this->error(" ❌ {$table} 遷移失敗: " . $e->getMessage());
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->newLine();
|
|
|
|
|
$this->info('🎉 資料遷移完成!');
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
}
|