後台資料庫 往中控傳 20250526

This commit is contained in:
allen.yan 2025-05-26 16:28:16 +08:00
parent 8bdab7c415
commit 4f37c90a0b
15 changed files with 868 additions and 57 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class ClearMachineStatuses extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:clear-machine-statuses';
/**
* The console command description.
*
* @var string
*/
protected $description = '備份並清空 machine_statuses 表,每週保留一次';
/**
* Execute the console command.
*/
public function handle()
{
$day = now()->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.");
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\ExportSqliteUserJob;
use App\Jobs\ExportSqliteSongJob;
use Illuminate\Support\Facades\Bus;
class ExportSqlite extends Command
{
/**
* 指令名稱與參數定義
*
* @var string
*/
protected $signature = 'export:sqlite
{type : The type of data to export (song, user, all)}
{--sync : Run the export job synchronously (without queue)}';
/**
* 指令描述
*
* @var string
*/
protected $description = 'Export data from the database to SQLite (song, user, or both).';
/**
* 執行指令
*/
public function handle()
{
$start = now();
$type = strtolower($this->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;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\ApiClient;
class SendSqliteFile extends Command
{
protected $signature = 'sqlite:send
{filename : The sqlite filename (e.g. tempUser.sqlite)}
{--url=https://ktvcentral.test/api/upload-sqlite : Target full API URL}
{--token= : Optional Bearer Token}
{--param=* : Optional extra POST data in key=value format (e.g. user_id=123)}';
protected $description = 'Send a sqlite file to remote server with optional token and data';
public function handle(): int
{
$filename = $this->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
*/

View File

@ -0,0 +1,62 @@
<?php
namespace App\Jobs;
use App\Services\SqliteExportService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
class ExportSqliteFavoriteJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 600; // 可依資料量調整 timeout 秒數
public function handle()
{
$sqlitePath = storage_path('app/database/tempFavorite.sqlite');
// 確保資料夾存在
if (!file_exists(dirname($sqlitePath))) {
mkdir(dirname($sqlitePath), 0755, true);
}
// 如果檔案不存在就建立空檔案
if (!file_exists($sqlitePath)) {
file_put_contents($sqlitePath, '');
}
config(['database.connections.tempsqlite' => [
'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);
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace App\Jobs;
use App\Models\Song;
use App\Models\Artist;
use App\Models\User;
use App\Services\SqliteExportService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
class ExportSqliteSongJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 600; // 可依資料量調整 timeout 秒數
public function handle()
{
$sqlitePath = storage_path('app/database/tempSong.sqlite');
// 確保資料夾存在
if (!file_exists(dirname($sqlitePath))) {
mkdir(dirname($sqlitePath), 0755, true);
}
// 如果檔案不存在就建立空檔案
if (!file_exists($sqlitePath)) {
file_put_contents($sqlitePath, '');
}
config(['database.connections.tempsqlite' => [
'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);
}
}

View File

@ -0,0 +1,205 @@
<?php
namespace App\Jobs;
use App\Models\Song;
use App\Models\Artist;
use App\Models\User;
use App\Services\SqliteExportService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
class ExportSqliteUserJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 600; // 可依資料量調整 timeout 秒數
public function handle()
{
$sqlitePath = storage_path('app/database/tempUser.sqlite');
// 確保資料夾存在
if (!file_exists(dirname($sqlitePath))) {
mkdir(dirname($sqlitePath), 0755, true);
}
// 如果檔案不存在就建立空檔案
if (!file_exists($sqlitePath)) {
file_put_contents($sqlitePath, '');
}
config(['database.connections.tempsqlite' => [
'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);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Jobs;
use App\Models\Branch;
use App\Services\ApiClient;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\{InteractsWithQueue, SerializesModels};
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class SendSqliteFileJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected string $filename;
protected ?int $branchId;
public function __construct(string $filename, ?int $branchId = null)
{
$this->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());
}
}
}
}

View File

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

View File

@ -0,0 +1,99 @@
<?php
namespace App\Services;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
class SqliteExportService
{
protected string $connection;
public function __construct()
{
$this->connection = 'tempsqlite';
}
/**
* 匯出單一模型資料到 SQLite 表。
*
* @param class-string<Model> $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<string, array{
* modelClass?: class-string<Model>,
* 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);
}
}

View File

@ -1,47 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('song_library_cache', function (Blueprint $table) {
$table->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');
}
};

View File

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

View File

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

View File

@ -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 專案已更新並執行完成。"

View File

@ -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 # 同步匯出所有