diff --git a/app/Jobs/ExportSqliteSongLibraryCacheJob.php b/app/Jobs/ExportSqliteSongLibraryCacheJob.php deleted file mode 100644 index ff64a80..0000000 --- a/app/Jobs/ExportSqliteSongLibraryCacheJob.php +++ /dev/null @@ -1,80 +0,0 @@ -delete(); - } else { - DB::table('song_library_cache')->truncate(); - } - } - - $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) { - SongLibraryCache::insert($chunk->toArray()); - $totalInserted += $chunk->count(); - }); - }); - // 你也可以 log 或通知插入結果 - // logger("Exported {$totalInserted} songs to cache."); - } -} diff --git a/app/Jobs/ImportSongChunkJob.php b/app/Jobs/ImportSongChunkJob.php index 94f4df4..6c8e472 100644 --- a/app/Jobs/ImportSongChunkJob.php +++ b/app/Jobs/ImportSongChunkJob.php @@ -5,6 +5,7 @@ namespace App\Jobs; use App\Models\Song; use App\Models\Artist; use App\Models\SongCategory; +use App\Models\SongLibraryCache; use App\Enums\ArtistCategory; use App\Enums\SongLanguageType; use App\Enums\SongSituation; @@ -23,11 +24,11 @@ class ImportSongChunkJob implements ShouldQueue use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected Collection $rows; - protected String $id; + protected string $id; protected array $categoryMap = []; - protected array $artistCache =[]; + protected array $artistCache = []; - public function __construct(Collection $rows,String $id) + public function __construct(Collection $rows, string $id) { $this->rows = $rows; $this->id = $id; @@ -36,13 +37,13 @@ class ImportSongChunkJob implements ShouldQueue public function handle(): void { - Log::warning('匯入啟動', [ - 'model' => "ImportSongChunkJob", - 'rows_id' =>$this->id, - ]); + Log::warning('匯入啟動', ['model' => "ImportSongChunkJob", 'rows_id' => $this->id]); + $ToInsert = []; + $songIdMap = []; $artistMap = []; $categoryMap = []; + $pMap = [ '\\\\SVR01\\DISK01\\' => 'DISK01\\', '\\\\SVR01\\DISK02\\' => 'DISK02\\', @@ -54,53 +55,37 @@ class ImportSongChunkJob implements ShouldQueue '\\\\SVR01\\DISK08\\' => 'DISK08\\', '\\\\SVR01\\DISK09\\' => 'DISK09\\', ]; + foreach ($this->rows as $index => $row) { $songId = trim($row['編號'] ?? ''); - - if (!$songId || Song::where('id', $songId)->exists()) { - continue; - } + if (!$songId || Song::where('id', $songId)->exists()) continue; try { - // 字元處理 - $songName=$this->normalizeName($row['歌名'] ?? ''); - $simplified=ChineseNameConverter::convertToSimplified($songName);// 繁體轉簡體 - if (!$row->has('注音')) { - $phoneticAbbr = ChineseNameConverter::getKTVZhuyinAbbr($simplified);// 注音符號 - } else { - $phoneticAbbr = trim($row['注音'] ?? ''); - } - if (!$row->has('拼音')) { - $pinyinAbbr = ChineseNameConverter::getKTVPinyinAbbr($simplified);// 拼音首字母 - } else { - $pinyinAbbr = trim($row['拼音'] ?? ''); - } - if (!$row->has('kk3')) {//歌名第一個字筆畫 - $chars = preg_split('//u', $songName, -1, PREG_SPLIT_NO_EMPTY); - $firstChar = $chars[0] ?? null; - $strokesAbbr=( $firstChar && preg_match('/\p{Han}/u', $firstChar) )? ChineseStrokesConverter::getStrokes($firstChar) : 0; - } else { - $strokesAbbr=trim($row['kk3'] ?? 0); - } - if (!$row->has('kk4')) {//歌名字數 - $songNumber = mb_strlen($songName, 'UTF-8'); - } else { - $songNumber=trim($row['kk4'] ?? 0); - } - $pathKey = trim($row['路徑01'] ?? ''); - $disk = $pMap[$pathKey] ?? ''; + $songName = $this->normalizeName($row['歌名'] ?? ''); + $simplified = ChineseNameConverter::convertToSimplified($songName); + $phoneticAbbr = $row->has('注音') ? trim($row['注音'] ?? '') : ChineseNameConverter::getKTVZhuyinAbbr($simplified); + $pinyinAbbr = $row->has('拼音') ? trim($row['拼音'] ?? '') : ChineseNameConverter::getKTVPinyinAbbr($simplified); + + $strokesAbbr = $row->has('kk3') + ? trim($row['kk3'] ?? 0) + : (preg_match('/\p{Han}/u', $songName[0] ?? '') ? ChineseStrokesConverter::getStrokes($songName[0]) : 0); + + $songNumber = $row->has('kk4') + ? trim($row['kk4'] ?? 0) + : mb_strlen($songName, 'UTF-8'); + + $disk = $pMap[trim($row['路徑01'] ?? '')] ?? ''; $filename = trim($row['檔名'] ?? ''); - // 準備 song 資料 + $ToInsert[] = [ 'id' => $songId, 'name' => $this->formatText($row['歌名']), 'adddate' => $this->parseExcelDate($row['日期'] ?? null), 'filename' => $disk . $filename, - //'language_type' => SongLanguageType::tryFrom(trim($row['語別'] ?? '')) ?? SongLanguageType::Unset, 'language_type' => SongLanguageType::fromLabelOrName($row['語別'] ?? ''), - 'db_change' => trim($row['kk2'] ?? 0),//分貝增減 - 'vocal' => trim($row['kk6'] ?? 0),//人聲 - 'situation' => SongSituation::tryFrom(trim($row['kk7'] ?? '')) ?? SongSituation::Unset,//情境 + 'db_change' => trim($row['kk2'] ?? 0), + 'vocal' => trim($row['kk6'] ?? 0), + 'situation' => SongSituation::tryFrom(trim($row['kk7'] ?? '')) ?? SongSituation::Unset, 'copyright01' => trim($row['版權01'] ?? ''), 'copyright02' => trim($row['版權02'] ?? ''), 'note01' => trim($row['版權03'] ?? ''), @@ -116,67 +101,58 @@ class ImportSongChunkJob implements ShouldQueue 'song_counts' => trim($row['點播次數'] ?? 0), ]; - // 處理關聯 - 歌手 - $artistIds = []; foreach (['歌星A', '歌星B'] as $key) { $artistName = $this->normalizeName($row[$key] ?? ''); - if ($artistName === '') continue; - - // 若是歌星B,且與歌星A相同,則跳過 - if ($key === '歌星B' && $artistName === $this->normalizeName($row['歌星A'] ?? '')) continue; - + if ($artistName === '' || ($key === '歌星B' && $artistName === $this->normalizeName($row['歌星A'] ?? ''))) continue; $artistMap[$songId][] = $this->getOrCreateArtistId($artistName); } - // 分類處理(多個用 , 分隔) if (!empty($row['分類'])) { - $categoryIds = []; - $codes = explode(',', $row['分類']); - foreach ($codes as $code) { + foreach (explode(',', $row['分類']) as $code) { $code = trim($code); if (isset($this->categoryMap[$code])) { $categoryMap[$songId][] = $this->categoryMap[$code]; } } } + + $songIdMap[] = $songId; } catch (\Throwable $e) { - \Log::error("Row {$index} failed: {$e->getMessage()}", [ + Log::error("Row {$index} failed: {$e->getMessage()}", [ 'row' => $row, 'trace' => $e->getTraceAsString() ]); } } - // 寫入資料庫 - Song::insert($ToInsert); - // 同步關聯(建議可用事件或批次處理) - foreach ($artistMap as $songId => $artistIds) { - $song = Song::find($songId); - if ($song) { - $song->artists()->sync($artistIds); + + // ✅ 批次插入 + collect($ToInsert)->chunk(500)->each(fn($chunk) => Song::insert($chunk->toArray())); + + // ✅ 一次撈資料並同步 + $songs = Song::whereIn('id', $songIdMap)->get()->keyBy('id'); + + foreach ($songs as $songId => $song) { + if (isset($artistMap[$songId])) { + $song->artists()->sync($artistMap[$songId]); } - } - foreach ($categoryMap as $songId => $categoryIds) { - $song = Song::find($songId); - if ($song) { - $song->categories()->sync($categoryIds); + if (isset($categoryMap[$songId])) { + $song->categories()->sync($categoryMap[$songId]); } + SongLibraryCache::syncFromSong($song); } } + private function getOrCreateArtistId(string $name): int { if (isset($this->artistCache[$name])) { return $this->artistCache[$name]; } - $artist = Artist::firstOrCreate( - ['name' => $name], - ['category' => ArtistCategory::Unset] - ); - + $artist = Artist::firstOrCreate(['name' => $name], ['category' => ArtistCategory::Unset]); return $this->artistCache[$name] = $artist->id; } - public function normalizeName(?string $str): string + private function normalizeName(?string $str): string { return strtoupper(mb_convert_kana(trim($str ?? ''), 'as')); } @@ -184,24 +160,21 @@ class ImportSongChunkJob implements ShouldQueue 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 \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value)->format('H:i'); } - return trim((string) $value); + return trim((string)$value); } + private function parseExcelDate($value): ?string { if (is_numeric($value)) { - return \Carbon\Carbon::createFromFormat('Y-m-d', '1900-01-01') - ->addDays((int)$value - 2) - ->format('Y-m-d'); + return \Carbon\Carbon::createFromFormat('Y-m-d', '1900-01-01')->addDays((int)$value - 2)->format('Y-m-d'); } try { return \Carbon\Carbon::parse($value)->format('Y-m-d'); - } catch (\Exception $e) { + } catch (\Exception) { return null; } } diff --git a/app/Livewire/Forms/SongForm.php b/app/Livewire/Forms/SongForm.php index 18f66a3..aaa1fef 100644 --- a/app/Livewire/Forms/SongForm.php +++ b/app/Livewire/Forms/SongForm.php @@ -12,13 +12,13 @@ use App\Enums\SongLanguageType; use App\Enums\SongSituation; use App\Models\Song; use App\Models\SongCategory; -use App\Jobs\ExportSqliteSongLibraryCacheJob; +use App\Models\SongLibraryCache; class SongForm extends Component { use WireUiActions; - protected $listeners = ['openModal','closeModal', 'deleteSong','synchronous']; + protected $listeners = ['openModal','closeModal', 'deleteSong']; public bool $canCreate; public bool $canEdit; @@ -70,6 +70,10 @@ class SongForm extends Component 'name' => $situation->labels(), 'value' => $situation->value, ])->toArray(); + $this->canCreate = Auth::user()?->can('song-edit') ?? false; + $this->canEdit = Auth::user()?->can('song-edit') ?? false; + $this->canDownload=Auth::user()?->can('song-delete') ?? false; + $this->canDelect = Auth::user()?->can('song-delete') ?? false; } public function openModal($id = null) @@ -91,16 +95,6 @@ class SongForm extends Component $this->resetFields(); $this->showModal = false; } - public function synchronous(): void - { - ExportSqliteSongLibraryCacheJob::dispatch(); - - $this->notification()->send([ - 'icon' => 'success', - 'title' => '歌庫_Cache 同步', - 'description' => '已經加入排程', - ]); - } public function save() { @@ -113,6 +107,7 @@ class SongForm extends Component // ⭐ 同步多對多關聯 $song->artists()->sync($this->selectedArtists ?? []); $song->categories()->sync($this->selectedCategories ?? []); + SongLibraryCache::syncFromSong($song); $this->notification()->send([ 'icon' => 'success', 'title' => '成功', @@ -126,6 +121,7 @@ class SongForm extends Component // ⭐ 同步多對多關聯 $song->artists()->sync($this->selectedArtists ?? []); $song->categories()->sync($this->selectedCategories ?? []); + SongLibraryCache::syncFromSong($song); $this->notification()->send([ 'icon' => 'success', 'title' => '成功', diff --git a/app/Livewire/Forms/SongTable.php b/app/Livewire/Forms/SongTable.php index 51fbbf5..2a4d3ac 100644 --- a/app/Livewire/Forms/SongTable.php +++ b/app/Livewire/Forms/SongTable.php @@ -3,6 +3,7 @@ namespace App\Livewire\Forms; use App\Models\Song; +use App\Models\SongLibraryCache; use App\Enums\SongLanguageType; use App\Enums\SongSituation; use Illuminate\Support\Carbon; @@ -136,6 +137,7 @@ final class SongTable extends PowerGridComponent ->add('note03') ->add('note04') ->add('enable') + ->add('song_counts') ->add('created_at_formatted', fn (Song $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s')); } @@ -173,6 +175,8 @@ final class SongTable extends PowerGridComponent $column[]=Column::make(__('songs.note04'), 'note04')->sortable()->searchable()->hidden(true, false) ->editOnClick(hasPermission: $this->canEdit, dataField: 'note04', fallback: 'N/A', saveOnMouseOut: true); $column[]=Column::make(__('songs.enable'), 'enable')->toggleable(hasPermission: $this->canEdit, trueLabel: 'yes', falseLabel: 'no'); + $column[]=Column::make(__('songs.song_counts'), 'song_counts')->sortable()->searchable() + ->editOnClick(hasPermission: $this->canEdit, dataField: 'song_counts', fallback: 'N/A', saveOnMouseOut: true); $column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable()->hidden(true, false); $column[]=Column::action(__('songs.actions')); return $column; @@ -197,6 +201,7 @@ final class SongTable extends PowerGridComponent Filter::inputText('note02')->placeholder(__('songs.note02')), Filter::inputText('note03')->placeholder(__('songs.note03')), Filter::inputText('note04')->placeholder(__('songs.note04')), + Filter::number('song_counts'), Filter::boolean('enable')->label('✅', '❌'), Filter::datetimepicker('created_at'), ]; @@ -229,7 +234,7 @@ final class SongTable extends PowerGridComponent public function onUpdatedEditable($id, $field, $value): void { if (in_array($field,[ - 'name','filename','db_change', + 'name','filename','db_change','song_counts', 'copyright01','copyright02','note01','note02','note03','note04' ]) && $this->canEdit) { $this->noUpdated($id,$field,$value); @@ -247,6 +252,7 @@ final class SongTable extends PowerGridComponent if ($song) { $song->{$field} = $value; $song->save(); // 明確觸發 saving + SongLibraryCache::syncFromSong($song); } $this->notification()->send([ 'icon' => 'success', diff --git a/app/Models/Song.php b/app/Models/Song.php index 27f85c1..69f7d63 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\SongLibraryCache; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Helpers\ChineseNameConverter; @@ -83,7 +84,9 @@ class Song extends Model $song->strokes_abbr=($firstChar && preg_match('/\p{Han}/u', $firstChar)) ? ChineseStrokesConverter::getStrokes($firstChar) : 0; $song->song_number = mb_strlen($song->name, 'UTF-8'); }); + static::deleting(function (Song $song) { + SongLibraryCache::where('song_id', $song->id)->delete(); // Detach 關聯資料 $song->artists()->detach(); $song->categories()->detach(); diff --git a/app/Models/SongLibraryCache.php b/app/Models/SongLibraryCache.php index 16ddd10..d029827 100644 --- a/app/Models/SongLibraryCache.php +++ b/app/Models/SongLibraryCache.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use App\Models\Song; class SongLibraryCache extends Model { @@ -38,5 +39,44 @@ class SongLibraryCache extends Model 'song_counts', 'updated_at', ]; + + public static function syncFromSong(Song $song): void + { + $song->load(['artists', 'categories']); + $sortedArtists = $song->artists->sortBy('id')->values(); + $artistA = $sortedArtists->get(0); + $artistB = $sortedArtists->get(1); + + static::updateOrCreate( + ['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(), + ] + ); + } } diff --git a/resources/lang/zh-tw/songs.php b/resources/lang/zh-tw/songs.php index f1a48f6..9a1a561 100644 --- a/resources/lang/zh-tw/songs.php +++ b/resources/lang/zh-tw/songs.php @@ -33,6 +33,7 @@ return [ 'vocal' => '人聲', 'situation' => '情境', 'simplified' => '歌名簡體', + 'song_counts' => '點播次數', 'select_artists' =>'輸入搜尋歌手', diff --git a/resources/views/livewire/header/song.blade.php b/resources/views/livewire/header/song.blade.php index 51b3be7..6453903 100644 --- a/resources/views/livewire/header/song.blade.php +++ b/resources/views/livewire/header/song.blade.php @@ -12,11 +12,4 @@ label="{{ __('songs.ImportData') }}" class="bg-green-600 text-white" /> - - \ No newline at end of file diff --git a/開發手冊.ini b/開發手冊.ini index b264140..e0aacce 100644 --- a/開發手冊.ini +++ b/開發手冊.ini @@ -40,4 +40,9 @@ IP 3F;pc301,pc302,pc303,pc305,pc306,pc307,pc308,pc309,pc310 3F;pc311,pc312,pc313,pc315,pc316,pc317,pc318,pc319,pc320 3F;pc321,pc322,pc323, -9F;pc901,pc902,pc903,pc910 \ No newline at end of file +9F;pc901,pc902,pc903,pc910 + +3F;svr01,svr02,svr03,svr04,svr05 +3F;pc301,pc302,pc303,pc305,pc306,pc307,pc308,pc309,pc310 +3F;pc311,pc312,pc313,pc315,pc316,pc317,pc318,pc319,pc320 +3F;pc321,pc322,pc323