後台資料庫 往中控傳 20250526
This commit is contained in:
parent
8bdab7c415
commit
4f37c90a0b
46
app/Console/Commands/ClearMachineStatuses.php
Normal file
46
app/Console/Commands/ClearMachineStatuses.php
Normal 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.");
|
||||
}
|
||||
}
|
88
app/Console/Commands/ExportSqlite.php
Normal file
88
app/Console/Commands/ExportSqlite.php
Normal 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;
|
||||
}
|
||||
}
|
71
app/Console/Commands/SendSqliteFile.php
Normal file
71
app/Console/Commands/SendSqliteFile.php
Normal 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
|
||||
*/
|
62
app/Jobs/ExportSqliteFavoriteJob.php
Normal file
62
app/Jobs/ExportSqliteFavoriteJob.php
Normal 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);
|
||||
}
|
||||
}
|
149
app/Jobs/ExportSqliteSongJob.php
Normal file
149
app/Jobs/ExportSqliteSongJob.php
Normal 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);
|
||||
}
|
||||
}
|
205
app/Jobs/ExportSqliteUserJob.php
Normal file
205
app/Jobs/ExportSqliteUserJob.php
Normal 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);
|
||||
|
||||
}
|
||||
}
|
55
app/Jobs/SendSqliteFileJob.php
Normal file
55
app/Jobs/SendSqliteFileJob.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
99
app/Services/SqliteExportService.php
Normal file
99
app/Services/SqliteExportService.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
};
|
@ -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)',
|
||||
|
@ -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
|
49
更新後部署流程(建議步驟).ini
Normal file
49
更新後部署流程(建議步驟).ini
Normal 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 專案已更新並執行完成。"
|
6
開發手冊.ini
6
開發手冊.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 # 同步匯出所有
|
Loading…
x
Reference in New Issue
Block a user