From 7dee52a00ad4a9650621cac3bf97a48dd664df47 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 22 May 2026 16:51:21 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E6=96=B0=E5=A2=9E=20OTA=20APK=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86=E8=88=87=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增 `ApkVersion` Model 與對應的資料表遷移檔 (`create_apk_versions_table`),用以記錄 APK 版本、代碼、風味、更新說明、下載連結與更新模式等欄位。 2. 實作 `ApkVersionController` 控制器,提供 APK 版本之 CRUD(列表、建立、刪除)以及 OTA 推送功能。 3. 移除了 `SpecialPermissionController` 中舊有的 `apkVersions` 占位路由方法,並於 `routes/web.php` 的基本設定 (`basic-settings`) 下新增完整的 APK 版本管理與推送路由。 4. 於 `RoleSeeder.php` 角色種子資料中為 Super Admin 與系統管理員新增 `menu.basic.apk-versions` 權限。 5. 於 `sidebar-menu.blade.php` 基本設定選單中,加入「APK版本管理」之子選單選項,並以對應權限進行防護。 6. 實作 APK 版本列表與上傳頁面 Blade 視圖,採用 Preline UI 與 Alpine.js 提供極簡奢華風的 UI 互動,包含檔案上傳拖曳與 OTA 設備選擇部署彈窗。 7. 更新繁體中文語系檔 `zh_TW.json`,對齊所有新增之 OTA 韌體與版本管理翻譯字詞。 --- .../BasicSettings/ApkVersionController.php | 118 ++++++ .../Admin/SpecialPermissionController.php | 9 - app/Models/Machine/ApkVersion.php | 31 ++ ...05_22_161500_create_apk_versions_table.php | 27 ++ database/seeders/RoleSeeder.php | 2 + lang/zh_TW.json | 49 ++- .../apk-versions/create.blade.php | 286 +++++++++++++ .../apk-versions/index.blade.php | 401 ++++++++++++++++++ .../layouts/partials/sidebar-menu.blade.php | 8 +- routes/web.php | 13 +- 10 files changed, 932 insertions(+), 12 deletions(-) create mode 100644 app/Http/Controllers/Admin/BasicSettings/ApkVersionController.php create mode 100644 app/Models/Machine/ApkVersion.php create mode 100644 database/migrations/2026_05_22_161500_create_apk_versions_table.php create mode 100644 resources/views/admin/basic-settings/apk-versions/create.blade.php create mode 100644 resources/views/admin/basic-settings/apk-versions/index.blade.php diff --git a/app/Http/Controllers/Admin/BasicSettings/ApkVersionController.php b/app/Http/Controllers/Admin/BasicSettings/ApkVersionController.php new file mode 100644 index 0000000..e284cd0 --- /dev/null +++ b/app/Http/Controllers/Admin/BasicSettings/ApkVersionController.php @@ -0,0 +1,118 @@ +paginate(10); + // 用於 OTA 下發時讓使用者選擇的機台列表 + $machines = Machine::all(); + return view('admin.basic-settings.apk-versions.index', compact('versions', 'machines')); + } + + public function create(): View + { + return view('admin.basic-settings.apk-versions.create'); + } + + public function store(Request $request) + { + $request->validate([ + 'version_name' => 'required|string|max:100', + 'version_code' => 'required|integer|unique:apk_versions,version_code', + 'flavor' => 'required|string|max:100', + 'apk_file' => 'required_without:download_url|nullable|file|max:102400', // 上傳限制 100MB + 'download_url' => 'required_without:apk_file|nullable|url|max:500', + 'release_notes' => 'nullable|string', + 'is_forced' => 'nullable|boolean', + ]); + + $filePath = null; + if ($request->hasFile('apk_file')) { + $filePath = $request->file('apk_file')->store('apks', 'public'); + } + + ApkVersion::create([ + 'version_name' => $request->version_name, + 'version_code' => $request->version_code, + 'flavor' => $request->flavor, + 'file_path' => $filePath, + 'download_url' => $request->download_url, + 'release_notes' => $request->release_notes, + 'is_forced' => $request->has('is_forced') || (bool) $request->input('is_forced', false), + ]); + + return redirect()->route('admin.basic-settings.apk-versions.index') + ->with('success', __('APK version uploaded successfully.')); + } + + public function destroy(ApkVersion $apkVersion) + { + if ($apkVersion->file_path) { + Storage::disk('public')->delete($apkVersion->file_path); + } + $apkVersion->delete(); + + return redirect()->route('admin.basic-settings.apk-versions.index') + ->with('success', __('APK version deleted successfully.')); + } + + /** + * 推送 OTA 更新指令給指定機台 + */ + public function push(Request $request, ApkVersion $apkVersion, MqttService $mqttService) + { + $request->validate([ + 'machine_ids' => 'required|array', + 'machine_ids.*' => 'exists:machines,id', + ]); + + $machines = Machine::whereIn('id', $request->machine_ids)->get(); + $successCount = 0; + + foreach ($machines as $machine) { + // 1. 先建立 RemoteCommand 紀錄,以取得自增 ID 作為 command_id(與系統慣例一致) + $command = \App\Models\Machine\RemoteCommand::create([ + 'machine_id' => $machine->id, + 'user_id' => auth()->id(), + 'command_type' => 'update_app', + 'status' => 'pending', + 'payload' => [ + 'url' => $apkVersion->url, + 'version' => $apkVersion->version_name, + 'version_code' => $apkVersion->version_code, + 'is_forced' => $apkVersion->is_forced, + ], + ]); + + // 2. 推播 MQTT,command_id 使用資料庫自增流水號 + $pushed = $mqttService->pushCommand( + $machine->serial_no, + 'update_app', + $command->payload, + (string) $command->id + ); + + if ($pushed) { + $successCount++; + } else { + // 推播失敗,將紀錄標記為失敗 + $command->update(['status' => 'failed']); + } + } + + return redirect()->route('admin.basic-settings.apk-versions.index') + ->with('success', sprintf(__('OTA command sent to %d devices successfully.'), $successCount)); + } +} diff --git a/app/Http/Controllers/Admin/SpecialPermissionController.php b/app/Http/Controllers/Admin/SpecialPermissionController.php index 1b304df..a1784e9 100644 --- a/app/Http/Controllers/Admin/SpecialPermissionController.php +++ b/app/Http/Controllers/Admin/SpecialPermissionController.php @@ -15,13 +15,4 @@ class SpecialPermissionController extends Controller 'description' => '特殊權限庫存清空功能', ]); } - - // APK版本管理 - public function apkVersions() - { - return view('admin.placeholder', [ - 'title' => 'APK版本管理', - 'description' => 'APP版本控制與更新', - ]); - } } diff --git a/app/Models/Machine/ApkVersion.php b/app/Models/Machine/ApkVersion.php new file mode 100644 index 0000000..597c1f0 --- /dev/null +++ b/app/Models/Machine/ApkVersion.php @@ -0,0 +1,31 @@ + 'boolean', + ]; + + /** + * 取得最終下載 URL + */ + public function getUrlAttribute(): string + { + return $this->download_url ?: Storage::disk('public')->url($this->file_path); + } +} diff --git a/database/migrations/2026_05_22_161500_create_apk_versions_table.php b/database/migrations/2026_05_22_161500_create_apk_versions_table.php new file mode 100644 index 0000000..9239590 --- /dev/null +++ b/database/migrations/2026_05_22_161500_create_apk_versions_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('version_name'); // 例如 '10_08_6_R' + $table->integer('version_code')->unique(); // 例如 100806,用於比對版本新舊,必須遞增 + $table->string('flavor'); // 機台風味,例如 'S_12_XY_U_Standard_' + $table->string('file_path')->nullable(); // 本地上傳的 APK 儲存路徑 + $table->string('download_url')->nullable(); // 外連下載 URL (如果未使用本地上傳) + $table->text('release_notes')->nullable(); // 版本更新說明 + $table->boolean('is_forced')->default(false); // 是否強制更新 + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('apk_versions'); + } +}; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index 7d9788f..6f46764 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -64,6 +64,7 @@ class RoleSeeder extends Seeder 'menu.basic.machines', 'menu.basic.payment-configs', 'menu.basic.discord-notifications', + 'menu.basic.apk-versions', 'menu.permissions', 'menu.permissions.companies', 'menu.permissions.accounts', @@ -125,6 +126,7 @@ class RoleSeeder extends Seeder 'menu.special-permission.apk-versions', 'menu.special-permission.discord-notifications', 'menu.basic.discord-notifications', + 'menu.basic.apk-versions', ]); } } \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index cf62518..83a9035 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -2151,6 +2151,7 @@ "menu.basic.discord-notifications": "Discord通知設定", "menu.basic.machines": "機台設定", "menu.basic.payment-configs": "客戶金流設定", + "menu.basic.apk-versions": "APK版本", "menu.data-config": "資料設定", "menu.data-config.admin-products": "商品狀態", "menu.data-config.advertisements": "廣告管理", @@ -2252,5 +2253,51 @@ "visit_gift": "來店禮", "vs Yesterday": "較昨日", "warehouses": "倉庫管理", - "warning": "警告" + "warning": "警告", + "OTA firmware update and version control": "OTA 韌體更新與版本控制", + "Upload New APK": "上傳新 APK", + "No versions found": "未找到任何版本", + "Are you sure you want to delete this APK version? This action will permanently remove the record and file.": "您確定要刪除此 APK 版本嗎?此操作將永久移除該紀錄與檔案。", + "OTA Update Deployment": "OTA 更新部署", + "Deploy version": "部署版本", + "Search machine name, S/N, or location...": "搜尋機台名稱、序號 (S/N) 或位置...", + "Online Only": "僅限線上機台", + "Machine Detail": "機台詳情", + "App Version": "App 版本", + "No machines match your criteria": "沒有符合條件的機台", + "Devices Selected": "個設備已選擇", + "Deploy Update": "部署更新", + "Register a new firmware version for OTA deployment": "註冊新韌體版本以進行 OTA 部署", + "Save Version": "儲存版本", + "Please enter version name": "請輸入版本名稱", + "Version code must be a positive integer": "版本代碼必須是正整數", + "Please select an APK file to upload": "請選擇要上傳的 APK 檔案", + "Please provide a download URL": "請提供下載連結", + "Version Information": "版本資訊", + "Firmware identification details": "韌體識別詳情", + "Version Name": "版本名稱", + "Format: Major_Minor_Patch_R": "格式:Major_Minor_Patch_R", + "Version Code": "版本代碼", + "Must be strictly greater than previous version": "必須嚴格大於上一個版本代碼", + "Machine Flavor": "機台風味", + "Select flavor...": "選擇風味...", + "Target hardware flavor for this APK": "此 APK 的目標硬體風味", + "Release Notes": "更新說明", + "Describe the changes in this version...": "描述此版本的變更...", + "APK Source": "APK 來源", + "Upload file or provide URL": "上傳檔案或提供連結", + "File Upload": "檔案上傳", + "External URL": "外連 URL", + "Drag & drop your APK here": "拖曳 APK 檔案至此", + "Maximum file size: 100MB": "檔案大小上限:100MB", + "Download URL": "下載連結", + "Provide a publicly accessible direct download link": "提供公開且可直接下載的連結", + "Update Mode": "更新模式", + "Control deployment behavior": "控制部署行為", + "Device will install immediately without user confirmation": "設備將立即安裝,無需使用者確認", + "Important Notice": "重要通知", + "Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.": "版本代碼必須嚴格大於先前的版本,否則機台端的 OTA 靜默安裝將會失敗並出現 INSTALL_FAILED_ALREADY_EXISTS 錯誤。", + "APK version uploaded successfully.": "APK 版本上傳成功。", + "APK version deleted successfully.": "APK 版本刪除成功。", + "OTA command sent to %d devices successfully.": "OTA 指令已成功發送至 %d 台設備。" } diff --git a/resources/views/admin/basic-settings/apk-versions/create.blade.php b/resources/views/admin/basic-settings/apk-versions/create.blade.php new file mode 100644 index 0000000..858158f --- /dev/null +++ b/resources/views/admin/basic-settings/apk-versions/create.blade.php @@ -0,0 +1,286 @@ +@extends('layouts.admin') + +@section('content') +
+ {{-- Header --}} +
+
+ + + +
+

{{ __('Upload New APK') }}

+

{{ __('Register a new firmware version for OTA deployment') }}

+
+
+
+ +
+
+ +
+ @csrf + + {{-- 錯誤提示 --}} + @if ($errors->any()) +
+
+ +
+
+

{{ __('Please check the following errors:') }}

+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+
+ @endif + +
+ {{-- 左欄:版本基本資訊 --}} +
+
+
+
+ +
+
+

{{ __('Version Information') }}

+

{{ __('Firmware identification details') }}

+
+
+ +
+
+ {{-- 版本名稱 --}} +
+ + +

{{ __('Format: Major_Minor_Patch_R') }}

+
+ {{-- 版本代碼 --}} +
+ + +

{{ __('Must be strictly greater than previous version') }}

+
+
+ + {{-- 機台風味 --}} +
+ + +

{{ __('Target hardware flavor for this APK') }}

+
+ + {{-- 更新說明 --}} +
+ + +
+
+
+
+ + {{-- 右欄:檔案上傳與更新模式 --}} +
+ {{-- 上傳模式卡片 --}} +
+
+
+ +
+
+

{{ __('APK Source') }}

+

{{ __('Upload file or provide URL') }}

+
+
+ + {{-- 上傳模式切換 --}} +
+ + +
+ + {{-- 檔案上傳區域 --}} +
+
+ + + +
+
+ + +
+ +
+

+ {{ __('Drag & drop your APK here') }} +

+

+

+ {{ __('Maximum file size: 100MB') }} · .apk +

+
+
+
+
+ + {{-- 外連 URL 輸入區域 --}} +
+ + +

{{ __('Provide a publicly accessible direct download link') }}

+
+
+ + {{-- 更新模式卡片 --}} +
+
+
+ +
+
+

{{ __('Update Mode') }}

+

{{ __('Control deployment behavior') }}

+
+
+ + {{-- 強制更新開關 --}} +
+
+
+ +
+
+

{{ __('Forced Update') }}

+

{{ __('Device will install immediately without user confirmation') }}

+
+
+ +
+ + {{-- 注意事項 --}} +
+
+ +
+

{{ __('Important Notice') }}

+

{{ __('Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.') }}

+
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/basic-settings/apk-versions/index.blade.php b/resources/views/admin/basic-settings/apk-versions/index.blade.php new file mode 100644 index 0000000..0c73488 --- /dev/null +++ b/resources/views/admin/basic-settings/apk-versions/index.blade.php @@ -0,0 +1,401 @@ +@extends('layouts.admin') + +@php + $machinesJson = $machines->map(function($m) { + return [ + 'id' => $m->id, + 'name' => $m->name, + 'serial_no' => $m->serial_no, + 'status' => $m->status, // online / offline + 'firmware_version' => $m->firmware_version ?? '', // 設備目前的版本或風味 + 'location' => $m->location ?? '', + ]; + }); +@endphp + +@section('content') + + +
+ +
+
+

{{ __('APK Versions') }}

+

{{ __('OTA firmware update and version control') }}

+
+
+ + + {{ __('Upload New APK') }} + +
+
+ + +
+ + +
+
+
+
+
+

{{ __('Loading Data') }}...

+
+ + @if(session('success')) +
+ + {{ session('success') }} +
+ @endif + + +
+
+ + + + + + + + + + + + @forelse($versions as $version) + + + + + + + + @empty + + + + @endforelse + +
{{ __('Version Info') }}{{ __('Machine Flavor') }}{{ __('Update Mode') }}{{ __('Release Notes') }}{{ __('Action') }}
+
+
+ +
+
+
+ {{ $version->version_name }} +
+
+ Code: {{ $version->version_code }} +
+
+
+
+ + {{ $version->flavor }} + + + @if($version->is_forced) + + {{ __('Forced Update') }} + + @else + + {{ __('Optional Update') }} + + @endif + + + {{ $version->release_notes ?: '-' }} + + + + + + + + + + + +
+ @csrf + @method('DELETE') + +
+
+
+ +
+

{{ __('No versions found') }}

+
+
+ +
+ {{ $versions->links('vendor.pagination.luxury') }} +
+
+
+ + + + + +
+ + +
+ + +
+
+ + +
+
+

+ + + + {{ __('OTA Update Deployment') }} +

+

+ {{ __('Deploy version') }}: () +

+
+ +
+ + +
+ +
+
+ + + + + + + +
+ +
+ +
+
+ + +
+ @csrf +
+ + + + + + + + + + + + + + + + +
+ + {{ __('Machine Detail') }}{{ __('Status') }}{{ __('App Version') }}
+ {{ __('No machines match your criteria') }} +
+
+
+
+ + +
+
+ {{ __('Devices Selected') }} +
+
+ + +
+
+ +
+
+
+ +
+@endsection diff --git a/resources/views/layouts/partials/sidebar-menu.blade.php b/resources/views/layouts/partials/sidebar-menu.blade.php index ac8e2b3..1147c1d 100644 --- a/resources/views/layouts/partials/sidebar-menu.blade.php +++ b/resources/views/layouts/partials/sidebar-menu.blade.php @@ -448,7 +448,7 @@ @endcan --}} -@if(auth()->user()->isSystemAdmin() || (auth()->user()->isTenant() && (auth()->user()->can('menu.basic.machines') || auth()->user()->can('menu.basic.payment-configs') || auth()->user()->can('menu.basic.discord-notifications')))) +@if(auth()->user()->isSystemAdmin() || (auth()->user()->isTenant() && (auth()->user()->can('menu.basic.machines') || auth()->user()->can('menu.basic.payment-configs') || auth()->user()->can('menu.basic.discord-notifications') || auth()->user()->can('menu.basic.apk-versions')))) {{-- 14.5. 基本設定 --}}