diff --git a/.DS_Store b/.DS_Store index 45ce0e8..0171a60 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app/Console/Commands/TransferSqliteToMysql.php b/app/Console/Commands/TransferSqliteToMysql.php new file mode 100644 index 0000000..87b6aa0 --- /dev/null +++ b/app/Console/Commands/TransferSqliteToMysql.php @@ -0,0 +1,76 @@ + 'sqlite', + 'database' => base_path('database/database.sqlite'), + 'prefix' => '', + ]); + + $this->info("🚀 Starting transfer from SQLite to MySQL..."); + + // 讀取 SQLite 資料庫的所有資料表 + $sqliteTables = DB::connection('sqlite')->select(" + SELECT name FROM sqlite_master + WHERE type='table' AND name NOT LIKE 'sqlite_%'; + "); + + if (empty($sqliteTables)) { + $this->error("❌ No tables found in SQLite database."); + return 1; + } + + // 取得 .env 中指定的 MySQL 連線名稱 + $mysqlConnection = config('database.default'); // 默認會是 'mysql',如果 .env 修改會自動更新 + + foreach ($sqliteTables as $tableObj) { + $table = $tableObj->name; + + // 忽略 Laravel 內部 migration 表 + if ($table === 'migrations') { + continue; + } + + $this->info("📦 Transferring table: {$table}"); + + try { + // 從 SQLite 資料庫讀取資料 + $records = DB::connection('sqlite')->table($table)->get(); + + if ($records->isEmpty()) { + $this->warn("⚠️ Table {$table} has no data."); + continue; + } + + // 資料分批處理,避免一次插入過多資料造成效能問題 + $chunks = $records->chunk(500); + foreach ($chunks as $chunk) { + DB::connection($mysqlConnection)->table($table)->insert( + $chunk->map(fn ($row) => (array) $row)->toArray() + ); + } + + $this->info("✅ Done: {$table}"); + } catch (\Exception $e) { + $this->error("❌ Failed to transfer {$table}: " . $e->getMessage()); + } + } + + $this->info("🎉 Transfer complete!"); + return 0; + } +} \ No newline at end of file diff --git a/app/Jobs/ImportArtistChunkJob.php b/app/Jobs/ImportArtistChunkJob.php index 5a182b8..902f8f6 100644 --- a/app/Jobs/ImportArtistChunkJob.php +++ b/app/Jobs/ImportArtistChunkJob.php @@ -24,23 +24,23 @@ class ImportArtistChunkJob implements ShouldQueue public function handle(): void { + $artists = collect(); + foreach ($this->rows as $index => $row) { try { $name = trim($row['歌手姓名'] ?? ''); - if (empty($name)) { - continue; - } - if (Artist::where('name', $name)->exists()) { + if (empty($name) || Artist::where('name', $name)->exists()) { continue; } - Artist::create([ + $artist = new Artist([ 'name' => $name, 'category' => ArtistCategory::tryFrom(trim($row['歌手分類'] ?? '未定義')) ?? ArtistCategory::Unset, 'enable' => trim($row['狀態'] ?? 1), ]); + $artists->push($artist); } catch (\Throwable $e) { \Log::error("Row {$index} failed: {$e->getMessage()}", [ 'row' => $row, @@ -48,5 +48,6 @@ class ImportArtistChunkJob implements ShouldQueue ]); } } + $artists->each(fn ($artist) => $artist->save()); } } \ No newline at end of file diff --git a/app/Jobs/ImportSongChunkJob.php b/app/Jobs/ImportSongChunkJob.php index 6810508..5f051b0 100644 --- a/app/Jobs/ImportSongChunkJob.php +++ b/app/Jobs/ImportSongChunkJob.php @@ -48,7 +48,7 @@ class ImportSongChunkJob implements ShouldQueue // 準備 song 資料 $song = new Song([ 'id' => $songId, - 'name' => trim($row['歌名'] ?? ''), + 'name' => $this->formatText($row['歌名']), 'adddate' => $this->parseExcelDate($row['日期'] ?? null), 'filename' => trim($row['檔名'] ?? ''), 'language_type' => SongLanguageType::tryFrom(trim($row['語別'] ?? '')) ?? SongLanguageType::Unset, @@ -113,6 +113,16 @@ class ImportSongChunkJob implements ShouldQueue return $this->artistCache[$name] = $artist->id; } + protected function formatText($value) + { + if (is_numeric($value) && $value < 1 && $value > 0) { + // 嘗試判斷為時間類型的小數,轉為時間字串 + $time = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value); + return $time->format('H:i'); + } + + return trim((string) $value); + } private function parseExcelDate($value): ?string { if (is_numeric($value)) { diff --git a/app/Models/Artist.php b/app/Models/Artist.php index e02bfbe..de7989d 100644 --- a/app/Models/Artist.php +++ b/app/Models/Artist.php @@ -45,7 +45,7 @@ class Artist extends Model $chars = preg_split('//u', $artist->name, -1, PREG_SPLIT_NO_EMPTY); $firstChar = $chars[0] ?? null; - $artist->strokes_abbr=$firstChar ? ChineseStrokesConverter::getStrokes($firstChar) : null; + $artist->strokes_abbr=( $firstChar && preg_match('/\p{Han}/u', $firstChar) ) ? ChineseStrokesConverter::getStrokes($firstChar) : 0; }); } diff --git a/app/Models/Song.php b/app/Models/Song.php index a3a3258..762e330 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -78,7 +78,8 @@ class Song extends Model $chars = preg_split('//u', $song->name, -1, PREG_SPLIT_NO_EMPTY); $firstChar = $chars[0] ?? null; - $song->strokes_abbr=$firstChar ? ChineseStrokesConverter::getStrokes($firstChar) : null; + + $song->strokes_abbr=($firstChar && preg_match('/\p{Han}/u', $firstChar)) ? ChineseStrokesConverter::getStrokes($firstChar) : 0; $song->song_number = mb_strlen($song->name, 'UTF-8'); }); } diff --git a/database/.DS_Store b/database/.DS_Store index b1ea27f..7805795 100644 Binary files a/database/.DS_Store and b/database/.DS_Store differ diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php index 425e705..bdd0253 100644 --- a/database/migrations/0001_01_01_000002_create_jobs_table.php +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -16,8 +16,8 @@ return new class extends Migration $table->string('queue')->index(); $table->longText('payload'); $table->unsignedTinyInteger('attempts'); - $table->unsignedInteger('reserved_at')->nullable(); - $table->unsignedInteger('available_at'); + $table->timestamp('reserved_at')->nullable(); + $table->timestamp('available_at'); $table->unsignedInteger('created_at'); }); @@ -29,9 +29,9 @@ return new class extends Migration $table->integer('failed_jobs'); $table->longText('failed_job_ids'); $table->mediumText('options')->nullable(); - $table->integer('cancelled_at')->nullable(); - $table->integer('created_at'); - $table->integer('finished_at')->nullable(); + $table->timestamp('cancelled_at')->nullable(); + $table->timestamp('created_at'); + $table->timestamp('finished_at')->nullable(); }); Schema::create('failed_jobs', function (Blueprint $table) { diff --git a/database/seeders/SqliteToMysqlSeeder.php b/database/seeders/KSongDatabaseToMysqlSeeder.php similarity index 100% rename from database/seeders/SqliteToMysqlSeeder.php rename to database/seeders/KSongDatabaseToMysqlSeeder.php diff --git a/storage/.DS_Store b/storage/.DS_Store index 8a1a6ec..9e38deb 100644 Binary files a/storage/.DS_Store and b/storage/.DS_Store differ diff --git a/開發手冊.ini b/開發手冊.ini index a93bc50..2ced5c6 100644 --- a/開發手冊.ini +++ b/開發手冊.ini @@ -104,4 +104,10 @@ cp .env.example .env php artisan key:generate npm install && npm run build -php artisan migrate \ No newline at end of file +php artisan migrate + + + +php artisan migrate:rollback +php artisan migrate +php artisan transfer:sqlite-to-mysql \ No newline at end of file