star-cloud/app/Http/Controllers/Admin/BasicSettings/ApkVersionController.php
sky121113 fb1bcf49e1 [FEAT] 新增 OTA 預約排程與部署狀態展示,並優化版本排序與 UI
1. 建立 `ota_schedules` 資料表與 `OtaSchedule` Model,提供 OTA 預約排程基礎儲存結構。
2. 建立 `ota:process-schedules` 背景 Artisan Command 並註冊至 Console Kernel,每分鐘自動執行到期的預約並透過 MQTT 發送。
3. 擴充 `ApkVersionController` index 方法:加入加載列表時自動觸發排程的防呆機制,並在記憶體中快速統計 pending 與 completed 的機台。
4. 擴充 `ApkVersionController` store 與 push 方法,移除已用不到的 `is_forced` (更新模式) 限制,並支援 `scheduled_at` 預約時間的寫入。
5. 調整 `create.blade.php` 頁面,移除「強制更新」的 Switch 欄位,僅保留注意事項防錯提示。
6. 調整 `index.blade.php` 頁面,將「更新模式」改為「部署狀態」一欄,整合系統內建的 `.tooltip` 與原生 `title` 屬性,在加載與刷新時 100% 穩定展示 Hover 機台名單。
7. 優化 `index.blade.php` 彈窗,將原生時間輸入框改為 Flatpickr 樣式,加入日曆圖標並支援動態 locale 語言切換(中/英/日),將標籤統一命名為「選擇更新時間」。
8. 同步更新並對齊中、英、日三語系 `lang/*.json` 語系檔,並完成字母排序與反逸出斜線處理。
2026-05-29 10:55:23 +08:00

166 lines
6.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Admin\BasicSettings;
use App\Http\Controllers\Admin\AdminController;
use App\Models\Machine\ApkVersion;
use App\Models\Machine\Machine;
use App\Services\Machine\MqttService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class ApkVersionController extends AdminController
{
public function index(): View
{
// 在進入 APK 版本清單時,同步觸發已到期排程的執行(確保無 cron 的開發/測試環境也能 100% 執行)
try {
\Illuminate\Support\Facades\Artisan::call('ota:process-schedules');
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Auto-trigger scheduled OTA failed: ' . $e->getMessage());
}
$versions = ApkVersion::latest()->paginate(10);
// 統計並附屬機台部署狀態
$commands = \App\Models\Machine\RemoteCommand::where('command_type', 'update_app')
->with('machine')
->get();
foreach ($versions as $version) {
$pending = [];
$completed = [];
foreach ($commands as $cmd) {
$cmdVersionCode = $cmd->payload['version_code'] ?? null;
if ($cmdVersionCode == $version->version_code) {
if ($cmd->status === 'completed' || $cmd->status === 'success') {
$completed[] = $cmd->machine;
} elseif ($cmd->status === 'pending' || $cmd->status === 'processing') {
$pending[] = $cmd->machine;
}
}
}
$version->pending_machines = collect($pending)->filter()->unique('id');
$version->completed_machines = collect($completed)->filter()->unique('id');
}
// 用於 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',
]);
$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' => 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',
'scheduled_at' => 'nullable|date|after:now',
]);
// 如果設定了預約發送時間
if ($request->filled('scheduled_at')) {
\App\Models\Machine\OtaSchedule::create([
'apk_version_id' => $apkVersion->id,
'scheduled_at' => $request->scheduled_at,
'target_type' => 'custom',
'target_value' => $request->machine_ids,
'status' => 'pending',
'user_id' => auth()->id(),
]);
return redirect()->route('admin.basic-settings.apk-versions.index')
->with('success', __('OTA update scheduled successfully.'));
}
$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. 推播 MQTTcommand_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));
}
}