[FEAT] 遠端指令中心優化與規格同步
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m11s

1. 實作遠端指令去重機制 (Supersede):避免重複下達相同待執行指令。
2. 修正遠端指令發送後的 Toast 提示邏輯,確保頁面跳轉後正確顯示回饋。
3. 增加 RemoteCommand 操作者 (user_id) 紀錄與狀態列舉擴充 (superseded)。
4. 修復機台列表「最後頁面」欄位對照錯誤,同步更新 Machine Model 與 API 規格。
5. 優化遠端指令中心 UI:放大卡片字體、調整側面欄間距,符合極簡奢華風規範。
6. 更新 API 技術規格書 (SKILL.md) 與 config/api-docs.php,補全所有機台代碼 (66-611) 與指令。
7. 補全繁體中文、英文、日文多語系翻譯檔案。
This commit is contained in:
2026-04-02 14:57:41 +08:00
parent e7ad7e3dc3
commit e085058d63
16 changed files with 1775 additions and 532 deletions

View File

@@ -131,7 +131,9 @@ class MachineController extends AdminController
'batch_no' => 'nullable|string|max:50',
]);
$this->machineService->updateSlot($machine, $validated);
$this->machineService->updateSlot($machine, $validated, auth()->id());
session()->flash('success', __('Slot updated successfully.'));
return response()->json([
'success' => true,

View File

@@ -15,18 +15,32 @@ class RemoteController extends Controller
*/
public function index(Request $request)
{
$machines = Machine::withCount(['slots'])->orderBy('name')->get();
$machines = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get();
$selectedMachine = null;
$history = RemoteCommand::where('command_type', '!=', 'reload_stock')->with(['machine', 'user'])->latest()->limit(50)->get();
if ($request->has('machine_id')) {
$selectedMachine = Machine::with(['slots.product', 'commands' => function($query) {
$query->latest()->limit(10);
$query->where('command_type', '!=', 'reload_stock')
->latest()
->limit(10);
}])->find($request->machine_id);
}
if ($request->ajax()) {
return response()->json([
'success' => true,
'machine' => $selectedMachine,
'commands' => $selectedMachine ? $selectedMachine->commands : []
]);
}
return view('admin.remote.index', [
'machines' => $machines,
'selectedMachine' => $selectedMachine,
'history' => $history,
'title' => __('Remote Command Center'),
'subtitle' => __('Execute maintenance and operational commands remotely')
]);
}
@@ -50,15 +64,35 @@ class RemoteController extends Controller
$payload['slot_no'] = $validated['slot_no'];
}
// 指令去重:將同機台、同類型的 pending 指令標記為「已取代」
RemoteCommand::where('machine_id', $validated['machine_id'])
->where('command_type', $validated['command_type'])
->where('status', 'pending')
->update([
'status' => 'superseded',
'note' => __('Superseded by new command'),
'executed_at' => now(),
]);
RemoteCommand::create([
'machine_id' => $validated['machine_id'],
'user_id' => auth()->id(),
'command_type' => $validated['command_type'],
'payload' => $payload,
'status' => 'pending',
'note' => $validated['note'] ?? null,
]);
return redirect()->back()->with('success', __('Command has been queued successfully.'));
session()->flash('success', __('Command has been queued successfully.'));
if ($request->expectsJson()) {
return response()->json([
'success' => true,
'message' => __('Command has been queued successfully.')
]);
}
return redirect()->back();
}
/**
@@ -70,17 +104,31 @@ class RemoteController extends Controller
'slots as slots_count',
'slots as low_stock_count' => function ($query) {
$query->where('stock', '<=', 5);
},
'slots as expiring_soon_count' => function ($query) {
$query->whereNotNull('expiry_date')
->where('expiry_date', '<=', now()->addDays(7))
->where('expiry_date', '>=', now()->startOfDay());
}
])->orderBy('name')->get();
])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get();
$history = RemoteCommand::where('command_type', 'reload_stock')->with(['machine', 'user'])->latest()->limit(50)->get();
$selectedMachine = null;
if ($request->has('machine_id')) {
$selectedMachine = Machine::with('slots.product')->find($request->machine_id);
$selectedMachine = Machine::with(['slots.product', 'commands' => function($query) {
$query->where('command_type', 'reload_stock')
->latest()
->limit(50);
}])->find($request->machine_id);
}
return view('admin.remote.stock', [
'machines' => $machines,
'selectedMachine' => $selectedMachine,
'history' => $history,
'title' => __('Stock & Expiry Management'),
'subtitle' => __('Real-time monitoring and adjustment of cargo lane inventory and expiration dates')
]);
}
}

View File

@@ -104,6 +104,12 @@ class MachineController extends Controller
{
$machine = $request->get('machine');
$slots = $machine->slots()->with('product')->get();
// 自動轉 Success: 若機台來撈 B017代表之前的 reload_stock 指令已成功被機台響應
\App\Models\Machine\RemoteCommand::where('machine_id', $machine->id)
->where('command_type', 'reload_stock')
->where('status', 'sent')
->update(['status' => 'success', 'executed_at' => now()]);
return response()->json([
'success' => true,

View File

@@ -106,6 +106,11 @@ class Machine extends Model
return $this->hasMany(MachineSlot::class);
}
public function commands()
{
return $this->hasMany(RemoteCommand::class);
}
public function machineModel()
{
return $this->belongsTo(MachineModel::class);
@@ -133,21 +138,20 @@ class Machine extends Model
'3' => 'Admin Page',
'4' => 'Replenishment Page',
'5' => 'Tutorial Page',
'60' => 'Purchasing',
'61' => 'Locked Page',
'62' => 'Dispense Failed',
'301' => 'Slot Test',
'302' => 'Slot Test',
'401' => 'Payment Selection',
'402' => 'Waiting for Payment',
'403' => 'Dispensing',
'404' => 'Receipt Printing',
'601' => 'Pass Code',
'602' => 'Pickup Code',
'603' => 'Message Display',
'604' => 'Cancel Purchase',
'605' => 'Purchase Finished',
'611' => 'Welcome Gift Status',
'6' => 'Purchasing',
'7' => 'Locked Page',
'60' => 'Dispense Success',
'61' => 'Slot Test',
'62' => 'Payment Selection',
'63' => 'Waiting for Payment',
'64' => 'Dispensing',
'65' => 'Receipt Printing',
'66' => 'Pass Code',
'67' => 'Pickup Code',
'68' => 'Message Display',
'69' => 'Cancel Purchase',
'610' => 'Purchase Finished',
'611' => 'Welcome Gift',
'612' => 'Dispense Failed',
];

View File

@@ -11,6 +11,7 @@ class RemoteCommand extends Model
protected $fillable = [
'machine_id',
'user_id',
'command_type',
'payload',
'status',
@@ -28,6 +29,11 @@ class RemoteCommand extends Model
return $this->belongsTo(Machine::class);
}
public function user()
{
return $this->belongsTo(\App\Models\System\User::class);
}
/**
* Scope for pending commands
*/

View File

@@ -4,6 +4,7 @@ namespace App\Services\Machine;
use App\Models\Machine\Machine;
use App\Models\Machine\MachineLog;
use App\Models\Machine\RemoteCommand;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
@@ -96,17 +97,25 @@ class MachineService
*
* @param Machine $machine
* @param array $data
* @param int|null $userId
* @return void
*/
public function updateSlot(Machine $machine, array $data): void
public function updateSlot(Machine $machine, array $data, ?int $userId = null): void
{
DB::transaction(function () use ($machine, $data) {
DB::transaction(function () use ($machine, $data, $userId) {
$slotNo = $data['slot_no'];
$stock = $data['stock'] ?? null;
$expiryDate = $data['expiry_date'] ?? null;
$batchNo = $data['batch_no'] ?? null;
$slot = $machine->slots()->where('slot_no', $slotNo)->firstOrFail();
// 紀錄舊數據以供 Payload 使用
$oldData = [
'stock' => $slot->stock,
'expiry_date' => $slot->expiry_date ? Carbon::parse($slot->expiry_date)->toDateString() : null,
'batch_no' => $slot->batch_no,
];
$updateData = [
'expiry_date' => $expiryDate,
'batch_no' => $batchNo,
@@ -114,6 +123,33 @@ class MachineService
if ($stock !== null) $updateData['stock'] = (int)$stock;
$slot->update($updateData);
// 指令去重:將該機台所有尚未領取的舊庫存同步指令標記為「已取代」
RemoteCommand::where('machine_id', $machine->id)
->where('command_type', 'reload_stock')
->where('status', 'pending')
->update([
'status' => 'superseded',
'note' => __('Superseded by new adjustment'),
'executed_at' => now(),
]);
// 建立遠端指令紀錄 (Unified Command Concept)
RemoteCommand::create([
'machine_id' => $machine->id,
'user_id' => $userId,
'command_type' => 'reload_stock',
'status' => 'pending',
'payload' => [
'slot_no' => $slotNo,
'old' => $oldData,
'new' => [
'stock' => $stock !== null ? (int)$stock : $oldData['stock'],
'expiry_date' => $expiryDate ?: null,
'batch_no' => $batchNo ?: null,
]
]
]);
});
}