From 4f37c90a0b2d0f64d1c53c3f19bf1773b507bf12 Mon Sep 17 00:00:00 2001 From: "allen.yan" Date: Mon, 26 May 2025 16:28:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=8C=E5=8F=B0=E8=B3=87=E6=96=99=E5=BA=AB?= =?UTF-8?q?=20=E5=BE=80=E4=B8=AD=E6=8E=A7=E5=82=B3=2020250526?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 10244 bytes app/Console/Commands/ClearMachineStatuses.php | 46 ++++ app/Console/Commands/ExportSqlite.php | 88 ++++++++ app/Console/Commands/SendSqliteFile.php | 71 ++++++ app/Jobs/ExportSqliteFavoriteJob.php | 62 ++++++ app/Jobs/ExportSqliteSongJob.php | 149 +++++++++++++ app/Jobs/ExportSqliteUserJob.php | 205 ++++++++++++++++++ app/Jobs/SendSqliteFileJob.php | 55 +++++ app/Services/ApiClient.php | 37 +++- app/Services/SqliteExportService.php | 99 +++++++++ ...031630_create_song_library_cache_table.php | 47 ---- database/seeders/CreateAdminUserSeeder.php | 3 + routes/console.php | 8 + 更新後部署流程(建議步驟).ini | 49 +++++ 開發手冊.ini | 6 + 15 files changed, 868 insertions(+), 57 deletions(-) create mode 100644 app/Console/Commands/ClearMachineStatuses.php create mode 100644 app/Console/Commands/ExportSqlite.php create mode 100644 app/Console/Commands/SendSqliteFile.php create mode 100644 app/Jobs/ExportSqliteFavoriteJob.php create mode 100644 app/Jobs/ExportSqliteSongJob.php create mode 100644 app/Jobs/ExportSqliteUserJob.php create mode 100644 app/Jobs/SendSqliteFileJob.php create mode 100644 app/Services/SqliteExportService.php delete mode 100644 database/migrations/2025_04_24_031630_create_song_library_cache_table.php create mode 100644 更新後部署流程(建議步驟).ini diff --git a/.DS_Store b/.DS_Store index 15a302ce922a8b9914077534916f0aaf7a36d788..2f7ce99ad3536cb5cc3718bcadde5f06263e29ee 100644 GIT binary patch delta 21 ccmZn(XbIS`S%AaH$XrLk$i#5-ZUIkm08UZ{fB*mh delta 21 ccmZn(XbIS`S%AaD*jz`!*u-G-ZUIkm08V-ag8%>k diff --git a/app/Console/Commands/ClearMachineStatuses.php b/app/Console/Commands/ClearMachineStatuses.php new file mode 100644 index 0000000..6645547 --- /dev/null +++ b/app/Console/Commands/ClearMachineStatuses.php @@ -0,0 +1,46 @@ +format('l'); // e.g. "Monday" + $targetTable = "machine_statuses_" . $day; + + DB::statement("CREATE TABLE IF NOT EXISTS _machine_statuses LIKE machine_statuses"); + + // 先刪除舊表(如存在) + DB::statement("DROP TABLE IF EXISTS {$targetTable}"); + + // 改名備份 + DB::statement("RENAME TABLE machine_statuses TO {$targetTable}"); + + // 空表回命名 + DB::statement("RENAME TABLE _machine_statuses TO machine_statuses"); + + $this->info("Machine statuses backed up to {$targetTable} and table cleared."); + } +} diff --git a/app/Console/Commands/ExportSqlite.php b/app/Console/Commands/ExportSqlite.php new file mode 100644 index 0000000..a72cc02 --- /dev/null +++ b/app/Console/Commands/ExportSqlite.php @@ -0,0 +1,88 @@ +argument('type')); + + $this->info("[Export] 開始匯出資料類型: {$type}"); + + try { + if (!in_array($type, ['song', 'user', 'all'])) { + $this->error('[Export] 無效的 type,請使用:song、user 或 all'); + return 1; + } + + $sync = $this->option('sync'); + + if ($sync) { + $this->warn('[Export] 使用同步模式執行...'); + + if ($type === 'song' || $type === 'all') { + (new ExportSqliteSongJob())->handle(); + } + if ($type === 'FavoriteSongs' || $type === 'all') { + (new ExportSqliteFavoriteJob())->handle(); + } + if ($type === 'user' || $type === 'all') { + (new ExportSqliteUserJob())->handle(); + } + + $this->info('[Export] 匯出完成(同步)'); + } else { + if ($type === 'all') { + // 確保 user -> song 順序 + Bus::chain([ + new ExportSqliteUserJob(), + new ExportSqliteFavoriteJob(), + new ExportSqliteSongJob(), + ])->dispatch(); + } elseif ($type === 'song') { + } elseif ($type === 'FavoriteSongs') { + ExportSqliteSongJob::dispatch(); + } elseif ($type === 'user') { + ExportSqliteUserJob::dispatch(); + } + + $this->info('[Export] 匯出任務已派送至 queue'); + } + + $duration = now()->diffInSeconds($start); + $this->info("[Export] 執行完成,用時 {$duration} 秒"); + + } catch (\Throwable $e) { + $this->error('[Export] 發生錯誤:' . $e->getMessage()); + return 1; + } + + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/SendSqliteFile.php b/app/Console/Commands/SendSqliteFile.php new file mode 100644 index 0000000..b687ea3 --- /dev/null +++ b/app/Console/Commands/SendSqliteFile.php @@ -0,0 +1,71 @@ +argument('filename'); + $url = $this->option('url'); + $token = $this->option('token'); + $params = $this->option('param'); // key=value + + $filePath = storage_path("app/database/{$filename}"); + + if (!file_exists($filePath)) { + $this->error("❌ 檔案不存在: {$filePath}"); + return 1; + } + + $endpoint = parse_url($url, PHP_URL_PATH); + $baseUrl = str_replace($endpoint, '', $url); + + // 處理額外參數 + $data = []; + foreach ($params as $pair) { + if (str_contains($pair, '=')) { + [$key, $value] = explode('=', $pair, 2); + $data[$key] = $value; + } + } + + $this->info("📤 傳送檔案 {$filename} 到 {$url} 中..."); + + try { + $client = new ApiClient($token, $baseUrl); + $response = $client->upload($endpoint, ['file' => $filePath], $data); + + if ($response->successful()) { + $this->info("✅ 傳送成功!"); + $this->info("🔁 回應內容: " . $response->body()); + } else { + $this->error("❌ 傳送失敗:HTTP {$response->status()}"); + $this->error($response->body()); + } + } catch (\Exception $e) { + $this->error("❌ 發生錯誤:" . $e->getMessage()); + return 1; + } + + return 0; + } +} + +/** + php artisan sqlite:send tempUser.sqlite \ + --url=https://ktvcentral.test/api/upload-sqlite \ + --token=abc123 \ + --param=user_id=888 --param=env=prod + */ \ No newline at end of file diff --git a/app/Jobs/ExportSqliteFavoriteJob.php b/app/Jobs/ExportSqliteFavoriteJob.php new file mode 100644 index 0000000..6a7072e --- /dev/null +++ b/app/Jobs/ExportSqliteFavoriteJob.php @@ -0,0 +1,62 @@ + [ + 'driver' => 'sqlite', + 'database' => $sqlitePath, + 'prefix' => '', + ]]); + + $exporter = new SqliteExportService(); + $exporter->exportMultiple([ + 'FavoriteSongs' => [ + 'query' => fn () => DB::table('FavoriteSongs'), + 'tableSchema' => function (Blueprint $table) { + $table->id(); + $table->string('songNumber',20); + $table->string('userPhone', 10); + $table->timestamps(); + }, + 'transformer' => fn ($row) => [ + 'songNumber' => $row->songNumber, + 'userPhone' => $row->userPhone, + 'created_at' => $row->created_at, + 'updated_at' => $row->updated_at, + ], + ], + ]); + SendSqliteFileJob::dispatch($sqlitePath); + } +} diff --git a/app/Jobs/ExportSqliteSongJob.php b/app/Jobs/ExportSqliteSongJob.php new file mode 100644 index 0000000..c2a19d6 --- /dev/null +++ b/app/Jobs/ExportSqliteSongJob.php @@ -0,0 +1,149 @@ + [ + 'driver' => 'sqlite', + 'database' => $sqlitePath, + 'prefix' => '', + ]]); + + Schema::connection('tempsqlite')->dropIfExists('song_library_cache'); + Schema::connection('tempsqlite')->create('song_library_cache', function (Blueprint $table) { + $table->bigIncrements('song_id')->comment('歌曲編號'); + $table->string('song_name')->nullable()->index()->comment('歌曲檔名'); + $table->string('song_simplified')->nullable()->index()->comment('歌曲簡體'); + $table->string('phonetic_abbr')->nullable()->index()->comment('歌曲注音'); + $table->string('pinyin_abbr')->nullable()->index()->comment('歌曲拼音'); + $table->integer('strokes_abbr')->default(0)->index()->comment('歌曲筆劃'); + $table->integer('song_number')->default(0)->index()->comment('歌曲字數'); + $table->string('artistA')->nullable()->index()->comment('歌星名稱A'); + $table->string('artistB')->nullable()->index()->comment('歌星名稱B'); + $table->string('artistA_simplified')->nullable()->index()->comment('歌星簡體名稱A'); + $table->string('artistB_simplified')->nullable()->index()->comment('歌星簡體名稱B'); + $table->string('artistA_category')->nullable()->default('未定義')->index()->comment('歌星類別A'); + $table->string('artistB_category')->nullable()->default('未定義')->index()->comment('歌星類別B'); + $table->string('artist_category')->nullable()->default('未定義')->index()->comment('歌星類別'); + $table->string('song_filename')->nullable()->comment('歌曲檔名'); + $table->string('song_category')->nullable()->comment('歌曲分類'); + $table->string('language_name')->nullable()->default('未定義')->index()->comment('語別'); + $table->date('add_date')->nullable()->index()->comment('新增日期'); + $table->string('situation')->nullable()->default('未定義')->index()->comment('情境'); + $table->tinyInteger('vocal')->default(0)->index()->comment('人聲'); // 0,1 + $table->integer('db_change')->default(0)->index()->comment('DB加減'); + $table->integer('song_counts')->default(0)->index()->comment('點播次數'); + $table->dateTime('updated_at')->nullable(); + }); + + $totalInserted = 0; + + Song::with(['artists', 'categories'])->chunk(500, function ($songs) use (&$totalInserted) + { + $rows = []; + + foreach ($songs as $song) { + $sortedArtists = $song->artists->sortBy('id')->values(); + $artistA = $sortedArtists->get(0); + $artistB = $sortedArtists->get(1); + + $rows[] = [ + 'song_id' => $song->id, + 'song_name' => $song->name, + 'song_simplified' => $song->simplified , + 'phonetic_abbr' => $song->phonetic_abbr ?? '', + 'pinyin_abbr' => $song->pinyin_abbr ?? '', + 'strokes_abbr' => $song->strokes_abbr ?? 0, + 'song_number' => $song->song_number ?? 0, + 'artistA' => $artistA?->name, + 'artistB' => $artistB?->name, + 'artistA_simplified' => $artistA?->simplified, + 'artistB_simplified' => $artistB?->simplified, + 'artistA_category' => $artistA?->category?->value ?? '未定義', + 'artistB_category' => $artistB?->category?->value ?? '未定義', + 'artist_category' => in_array(\App\Enums\ArtistCategory::Group->value, [ + $artistA?->category?->value, + $artistB?->category?->value, + ]) ? '團' : '未定義', + 'song_filename' => $song->filename, + 'song_category'=>$song->categories->pluck('code')->unique()->sort()->implode(', '), + 'language_name' => $song->language_type ?? '未定義', + 'add_date' => $song->adddate, + 'situation' => $song->situation?->value ?? '未定義', + 'vocal' => $song->vocal, + 'db_change' => $song->db_change, + 'song_counts' => $song->song_counts ?? 0, + 'updated_at' => now(), + ]; + } + + collect($rows)->chunk(1000)->each(function ($chunk) use (&$totalInserted) { + DB::connection('tempsqlite')->table('song_library_cache')->insert($chunk->toArray()); + $totalInserted += $chunk->count(); + }); + }); + + $exporter = new SqliteExportService(); + $exporter->exportMultiple([ + 'artists' => [ + 'modelClass' => Artist::class, + 'tableSchema' => function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('category')->default('未定義')->index(); + $table->string('name')->unique(); + $table->string('simplified')->index(); + $table->string('phonetic_abbr')->index(); + $table->string('pinyin_abbr')->index(); + $table->integer('strokes_abbr')->index(); + $table->tinyInteger('enable')->default(1); + $table->dateTime('updated_at')->nullable(); + }, + 'transformer' => fn (Artist $artist) => [ + 'id' => $artist->id, + 'category' => $artist->category?->value ?? '未定義', + 'name' => $artist->name, + 'simplified' => $artist->simplified, + 'phonetic_abbr' => $artist->phonetic_abbr ?? '', + 'pinyin_abbr' => $artist->pinyin_abbr ?? '', + 'strokes_abbr' => $artist->strokes_abbr ?? 0, + 'enable' => $artist->enable, + 'updated_at' => now(), + ], + ], + ]); + SendSqliteFileJob::dispatch($sqlitePath); + } +} diff --git a/app/Jobs/ExportSqliteUserJob.php b/app/Jobs/ExportSqliteUserJob.php new file mode 100644 index 0000000..bac8e24 --- /dev/null +++ b/app/Jobs/ExportSqliteUserJob.php @@ -0,0 +1,205 @@ + [ + 'driver' => 'sqlite', + 'database' => $sqlitePath, + 'prefix' => '', + ]]); + + $exporter = new SqliteExportService(); + $exporter->exportMultiple([ + // --- users --- + 'users' => [ + 'modelClass' => User::class, + 'tableSchema' => function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('phone', 10)->unique(); + $table->date('birthday')->nullable(); // 生日 + $table->string('gender')->default('unset'); // 性別 + $table->tinyInteger('status')->default(0); // 啟動 + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->text('api_plain_token')->nullable(); + $table->timestamps(); + }, + 'transformer' => fn (User $user) => [ + 'name' => $user->name, + 'email' => $user->email, + 'phone' => $user->phone, + 'birthday' => $user->birthday, + 'gender' => $user->gender?->value ?? 'unset', + 'status' => $user->status, + 'email_verified_at' => $user->email_verified_at, + 'password' => $user->password, + 'remember_token' => $user->remember_token, + 'api_plain_token' => $user->api_plain_token, + 'created_at' => $user->created_at, + 'updated_at' => $user->updated_at, + ], + ], + // --- password_reset_tokens --- + 'password_reset_tokens' => [ + 'query' => fn () => DB::table('password_reset_tokens'), + 'tableSchema' => function (Blueprint $table) { + $table->string('email')->index(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }, + 'transformer' => fn ($row) => [ + 'email' => $row->email, + 'token' => $row->token, + 'created_at' => $row->created_at, + ], + ], + // --- personal_access_tokens --- + 'personal_access_tokens' => [ + 'query' => fn () => DB::table('personal_access_tokens'), + 'tableSchema' => function (Blueprint $table) { + $table->id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }, + 'transformer' => fn ($row) => [ + 'id' => $row->id, + 'tokenable_type' => $row->tokenable_type, + 'tokenable_id' => $row->tokenable_id, + 'name' => $row->name, + 'token' => $row->token, + 'abilities' => $row->abilities, + 'last_used_at' => $row->last_used_at, + 'expires_at' => $row->expires_at, + 'created_at' => $row->created_at, + 'updated_at' => $row->updated_at, + ], + ], + // --- roles --- + 'roles' => [ + 'query' => fn () => DB::table('roles'), + 'tableSchema' => function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + }, + 'transformer' => fn ($row) => [ + 'id' => $row->id, + 'name' => $row->name, + 'guard_name' => $row->guard_name, + 'created_at' => $row->created_at, + 'updated_at' => $row->updated_at, + ], + ], + + // --- permissions --- + 'permissions' => [ + 'query' => fn () => DB::table('permissions'), + 'tableSchema' => function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + }, + 'transformer' => fn ($row) => [ + 'id' => $row->id, + 'name' => $row->name, + 'guard_name' => $row->guard_name, + 'created_at' => $row->created_at, + 'updated_at' => $row->updated_at, + ], + ], + + // --- role_has_permissions --- + 'role_has_permissions' => [ + 'query' => fn () => DB::table('role_has_permissions'), + 'tableSchema' => function (Blueprint $table) { + $table->unsignedBigInteger('permission_id'); + $table->unsignedBigInteger('role_id'); + $table->primary(['permission_id', 'role_id']); + }, + 'transformer' => fn ($row) => [ + 'permission_id' => $row->permission_id, + 'role_id' => $row->role_id, + ], + ], + + // --- model_has_roles --- + 'model_has_roles' => [ + 'query' => fn () => DB::table('model_has_roles'), + 'tableSchema' => function (Blueprint $table) { + $table->unsignedBigInteger('role_id'); + $table->string('model_type'); + $table->unsignedBigInteger('model_id'); + $table->primary(['role_id', 'model_id', 'model_type'], 'model_has_roles_primary'); + }, + 'transformer' => fn ($row) => [ + 'role_id' => $row->role_id, + 'model_type' => $row->model_type, + 'model_id' => $row->model_id, + ], + ], + + // --- model_has_permissions --- + 'model_has_permissions' => [ + 'query' => fn () => DB::table('model_has_permissions'), + 'tableSchema' => function (Blueprint $table) { + $table->unsignedBigInteger('permission_id'); + $table->string('model_type'); + $table->unsignedBigInteger('model_id'); + $table->index(['model_id', 'model_type']); + $table->primary(['permission_id', 'model_id', 'model_type']); + }, + 'transformer' => fn ($row) => [ + 'permission_id' => $row->permission_id, + 'model_type' => $row->model_type, + 'model_id' => $row->model_id, + ], + ], + ]); + SendSqliteFileJob::dispatch($sqlitePath); + + } +} diff --git a/app/Jobs/SendSqliteFileJob.php b/app/Jobs/SendSqliteFileJob.php new file mode 100644 index 0000000..fb085de --- /dev/null +++ b/app/Jobs/SendSqliteFileJob.php @@ -0,0 +1,55 @@ +filename = $filename; + $this->branchId = $branchId; + } + + public function handle(): void + { + $path = storage_path($this->filename); + + if (!file_exists($path)) { + Log::error("❌ SQLite 檔案不存在: {$path}"); + return; + } + $user = \App\Models\User::find(2); + $token = $user->api_plain_token; + + $branches = $this->branchId + ? Branch::where('id', $this->branchId)->where('enabled', true)->get() + : Branch::where('enabled', true)->cursor(); + + foreach ($branches as $branch) { + $client = new ApiClient($token, $branch->external_ip); + + $response = $client->upload('/api/upload-sqlite', ['file' => $path]); + + if ($response->successful()) { + Log::info("✅ 檔案 {$this->filename} 傳送成功"); + } else { + Log::error("❌ 傳送失敗:HTTP {$response->status()}"); + Log::error($response->body()); + } + } + } +} \ No newline at end of file diff --git a/app/Services/ApiClient.php b/app/Services/ApiClient.php index f231111..43728ea 100644 --- a/app/Services/ApiClient.php +++ b/app/Services/ApiClient.php @@ -8,34 +8,51 @@ class ApiClient protected string $baseUrl; protected string $token; - public function __construct(string $token = null) + public function __construct(string $token = null, ?string $baseUrl = null) { - $this->baseUrl = config('services.room_api.base_url', 'https://ktv.test/api'); + $this->baseUrl = rtrim($baseUrl ?? config('services.room_api.base_url', 'https://ktv.test/api'), '/'); $this->token = $token ?? config('services.room_api.token'); } + public function setToken(string $token): self { $this->token = $token; return $this; } + public function setBaseUrl(string $url): self + { + $this->baseUrl = rtrim($url, '/'); + return $this; + } + public function withDefaultHeaders(): \Illuminate\Http\Client\PendingRequest { return Http::withHeaders([ 'Accept' => 'application/json', - 'Authorization' => 'Bearer ' . config('services.room_api.token'), - 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $this->token, ]); } - public function post(string $endpoint, array $data = []) - { - return $this->withDefaultHeaders()->post($this->baseUrl . $endpoint, $data); - } - public function get(string $endpoint, array $query = []) { - return $this->withDefaultHeaders()->get($this->baseUrl . $endpoint, $query); + return $this->withDefaultHeaders()->get("{$this->baseUrl}{$endpoint}", $query); } + public function post(string $endpoint, array $data = []) + { + return $this->withDefaultHeaders()->post("{$this->baseUrl}{$endpoint}", $data); + } + + public function upload(string $endpoint, array $files = [], array $data = []) + { + $request = $this->withDefaultHeaders(); + + foreach ($files as $key => $filePath) { + $filename = basename($filePath); + $request = $request->attach($key, fopen($filePath, 'r'), $filename); + } + + return $request->withoutVerifying()->post("{$this->baseUrl}{$endpoint}", $data); + } } \ No newline at end of file diff --git a/app/Services/SqliteExportService.php b/app/Services/SqliteExportService.php new file mode 100644 index 0000000..f746fed --- /dev/null +++ b/app/Services/SqliteExportService.php @@ -0,0 +1,99 @@ +connection = 'tempsqlite'; + } + + /** + * 匯出單一模型資料到 SQLite 表。 + * + * @param class-string $modelClass + * @param string $tableName + * @param Closure(Blueprint): void $tableSchema + * @param Closure(Model): array $transformer + * @param int $chunkSize + */ + public function exportTableFromModel( + string $modelClass, + string $tableName, + Closure $tableSchema, + Closure $transformer, + int $chunkSize = 1000 + ): void { + $this->dropAndCreateTable($tableName, $tableSchema); + + $modelInstance = new $modelClass; + + $modelInstance->newQuery()->orderBy('id')->chunk($chunkSize, function (Collection $chunk) use ($tableName, $transformer) { + $rows = $chunk->map($transformer)->toArray(); + $this->insertData($tableName, $rows); + }); + } + + /** + * 批次匯出多張表 + * + * @param array, + * query?: Closure(): \Illuminate\Support\Collection, + * tableSchema: Closure(Blueprint): void, + * transformer: Closure(Model): array, + * chunkSize?: int + * }> $tables + */ + public function exportMultiple(array $tables): void + { + foreach ($tables as $tableName => $config) { + $this->dropAndCreateTable($tableName, $config['tableSchema']); + $transformer = $config['transformer'] ?? fn($row) => (array)$row; + + if (isset($config['modelClass'])) { + $modelClass = $config['modelClass']; + $chunkSize = $config['chunkSize'] ?? 1000; + $modelInstance = new $modelClass; + + $modelInstance->newQuery()->chunk($chunkSize, function (Collection $chunk) use ($tableName, $transformer) { + $rows = $chunk->map($transformer)->toArray(); + $this->insertData($tableName, $rows); + }); + } elseif (isset($config['query']) && is_callable($config['query'])) { + $rows = call_user_func($config['query']); + + if ($rows instanceof \Illuminate\Database\Query\Builder || $rows instanceof \Illuminate\Database\Eloquent\Builder) { + $rows = $rows->get(); + } + + $data = $rows->map($transformer)->toArray(); + $this->insertData($tableName, $data); + } else { + throw new \InvalidArgumentException("Each table config must define either 'modelClass' or 'query'."); + } + } + } + + protected function dropAndCreateTable(string $table, Closure $schema): void + { + Schema::connection($this->connection)->dropIfExists($table); + Schema::connection($this->connection)->create($table, $schema); + } + + protected function insertData(string $table, array $rows): void + { + if (empty($rows)) return; + DB::connection($this->connection)->table($table)->insert($rows); + } +} \ No newline at end of file diff --git a/database/migrations/2025_04_24_031630_create_song_library_cache_table.php b/database/migrations/2025_04_24_031630_create_song_library_cache_table.php deleted file mode 100644 index c8d7009..0000000 --- a/database/migrations/2025_04_24_031630_create_song_library_cache_table.php +++ /dev/null @@ -1,47 +0,0 @@ -integer('song_id')->primary(); - $table->string('song_name', 255)->nullable(); - $table->string('artistA', 255)->nullable(); - $table->string('artistB', 255)->nullable(); - $table->string('song_filename', 255)->nullable(); - $table->string('artistA_simplified', 255)->nullable(); - $table->string('ArtistB_simplified', 255)->nullable(); - $table->string('artistA_category', 255)->nullable(); - $table->string('artistB_category', 255)->nullable(); - $table->string('song_simplified', 255)->nullable(); - $table->string('situations', 255)->nullable(); - $table->string('vocal', 255)->nullable(); - $table->string('language_name', 50)->nullable(); - $table->string('phonetic_abbr', 255)->nullable(); - $table->string('artistA_phonetic', 255)->nullable(); - $table->string('artistB_phonetic', 255)->nullable(); - $table->string('artistA_pinyin', 255)->nullable(); - $table->string('artistB_pinyin', 255)->nullable(); - $table->string('pinyin_abbr', 255)->nullable(); - $table->integer('song_counts')->nullable(); - $table->dateTime('add_date')->nullable(); - $table->dateTime('updated_at')->nullable(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('song_library_cache'); - } -}; diff --git a/database/seeders/CreateAdminUserSeeder.php b/database/seeders/CreateAdminUserSeeder.php index de62fe2..471bb0d 100644 --- a/database/seeders/CreateAdminUserSeeder.php +++ b/database/seeders/CreateAdminUserSeeder.php @@ -29,6 +29,9 @@ class CreateAdminUserSeeder extends Seeder 'password' => bcrypt('aa147258-') ]); $user->assignRole('Machine'); + $token = $user->createToken('pc-heartbeat')->plainTextToken; + $user->api_plain_token = $token; + $user->save(); $user = User::create([ 'name' => 'Allen Yan(User)', diff --git a/routes/console.php b/routes/console.php index 3c9adf1..7757724 100644 --- a/routes/console.php +++ b/routes/console.php @@ -2,7 +2,15 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schedule; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + + +Schedule::command('machine_statuses:clear')->dailyAt('12:00'); // 每天凌晨 12:10 執行 +//首次部署或有新增命令時)建立或更新任務排程 Crontab +// 檢查是否已有下列 crontab 設定(crontab -e): +//分鐘 小時 日 月 星期 指令 +// * * * * * cd /Users/allen.yan/work/KTV && php artisan schedule:run >> /dev/null 2>&1 \ No newline at end of file diff --git a/更新後部署流程(建議步驟).ini b/更新後部署流程(建議步驟).ini new file mode 100644 index 0000000..372491e --- /dev/null +++ b/更新後部署流程(建議步驟).ini @@ -0,0 +1,49 @@ +✅ Laravel 更新後部署流程(建議步驟) + +1. 拉取新版程式碼 + git pull origin main + +2. 安裝依賴套件 + composer install --no-dev --optimize-autoloader + + +3. 執行資料庫 migration(如有 schema 變更) + php artisan migrate + +4. 清除並重新快取設定與路由 + php artisan config:clear + php artisan config:cache + php artisan route:clear + php artisan route:cache + php artisan view:clear + php artisan view:cache + +5. (首次部署或有新增命令時)建立或更新任務排程 Crontab + 檢查是否已有下列 crontab 設定(crontab -e): + 分鐘 小時 日 月 星期 指令 + * * * * * cd /path/to/your/project && php artisan schedule:run >> /dev/null 2>&1 + 這樣 Laravel 才能自動執行你在 routes/console.php 中定義的排程任務。 + +6. (選擇性)部署完立即執行某些 Artisan 指令 + 例如你可能希望部署後立即重建一次機器狀態資料表,可以執行: + php artisan machine_statuses:clear + +7. 權限與快取設定(根據伺服器環境) + 確認 storage 和 bootstrap/cache 目錄權限正確: + chmod -R 775 storage bootstrap/cache + chown -R www-data:www-data storage bootstrap/cache + +✅ 完整部署腳本範例(可寫成 deploy.sh) + #!/bin/bash + + cd /var/www/your-project + + git pull origin main + composer install --no-dev --optimize-autoloader + php artisan migrate --force + php artisan config:cache + php artisan route:cache + php artisan view:cache + php artisan machine_statuses:clear + + echo "✅ Laravel 專案已更新並執行完成。" \ No newline at end of file diff --git a/開發手冊.ini b/開發手冊.ini index a9486bc..6737491 100644 --- a/開發手冊.ini +++ b/開發手冊.ini @@ -129,3 +129,9 @@ composer require "darkaonline/l5-swagger" php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider" php -d memory_limit=512M artisan l5-swagger:generate +php artisan export:sqlite song --sync # 同步匯出歌曲 +php artisan export:sqlite user --sync # 同步匯出歌曲 +php artisan export:sqlite song # 同步匯出歌曲 +php artisan export:sqlite user # 非同步匯出使用者 +php artisan export:sqlite all # 非同步匯出所有 +php artisan export:sqlite all --sync # 同步匯出所有 \ No newline at end of file