star-cloud/app/Models/Machine/Machine.php
sky121113 b0e523067f [STYLE] 優化機台設定與倉庫管理介面規範化
1. 機台管理模組優化:
   - 移除機台設定內部的「機台分布」標籤頁,改為標題旁獨立的極簡白色連結入口。
   - 優化機台列表欄位,將「公司名稱」獨立顯示。
   - 實作機台系統設定非同步更新功能 (update-system-settings)。
2. 倉庫管理模組規範化:
   - 統一庫存分頁的視覺色調,使用一致的青色 (Cyan) 強調色。
   - 優化日期篩選器,支援精確時間 (H:i) 選擇並預設 00:00。
   - 實作自定義 multi-select 組件以解決 Preline 下拉衝突。
   - 確保所有篩選操作觸發立即的 AJAX 資料同步與狀態更新。
3. 系統基礎優化:
   - 更新繁體中文、日文、英文語系檔,確保標籤顯示正確。
   - 清理 Controller 多餘的地圖資料擷取邏輯。
2026-04-24 10:54:24 +08:00

278 lines
8.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\Models\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Traits\TenantScoped;
class Machine extends Model
{
use HasFactory, TenantScoped;
use \Illuminate\Database\Eloquent\SoftDeletes;
protected static function booted()
{
// 權限隔離:一般帳號登入時只能看到自己被分配的機台
static::addGlobalScope('machine_access', function (\Illuminate\Database\Eloquent\Builder $builder) {
$user = auth()->user();
// 如果是在 Console、或是系統管理員則不限制 (可看所有機台)
if (app()->runningInConsole() || !$user || $user->isSystemAdmin()) {
return;
}
// 一般租戶帳號:限制只能看自己擁有的機台
$builder->whereExists(function ($query) use ($user) {
$query->select(\Illuminate\Support\Facades\DB::raw(1))
->from('machine_user')
->whereColumn('machine_user.machine_id', 'machines.id')
->where('machine_user.user_id', $user->id);
});
});
// 當 api_token 或 serial_no 發生變動時,自動同步至 Redis (供 MQTT 認證使用)
static::saved(function ($machine) {
if ($machine->wasChanged('api_token') || $machine->wasChanged('serial_no') || $machine->wasRecentlyCreated) {
app(\App\Services\Machine\MachineService::class)->syncMqttAuth($machine);
// 如果是修改序號,則刪除舊的 Redis Key 避免殘留
if ($machine->wasChanged('serial_no') && !$machine->wasRecentlyCreated) {
$oldSerial = $machine->getOriginal('serial_no');
if ($oldSerial) {
\Illuminate\Support\Facades\Redis::del("machine_auth:{$oldSerial}");
}
}
}
});
}
protected $fillable = [
'company_id',
'name',
'serial_no',
'model',
'location',
'address',
'latitude',
'longitude',
'status',
'current_page',
'door_status',
'temperature',
'firmware_version',
'api_token',
'last_heartbeat_at',
'card_reader_seconds',
'card_reader_checkout_time_1',
'card_reader_checkout_time_2',
'heating_start_time',
'heating_end_time',
'payment_buffer_seconds',
'card_reader_no',
'key_no',
'invoice_status',
'welcome_gift_enabled',
'is_spring_slot_1_10',
'is_spring_slot_11_20',
'is_spring_slot_21_30',
'is_spring_slot_31_40',
'is_spring_slot_41_50',
'is_spring_slot_51_60',
'member_system_enabled',
'payment_config_id',
'machine_model_id',
'images',
'creator_id',
'updater_id',
'tax_invoice_enabled',
'card_terminal_enabled',
'scan_pay_esun_enabled',
'scan_pay_linepay_enabled',
'shopping_cart_enabled',
'cash_module_enabled',
];
protected $appends = ['image_urls', 'calculated_status'];
/**
* 動態計算機台當前狀態
* 優先讀取資料庫 status 欄位
* 若在線,則進一步檢查過去 15 分鐘是否為異常
*/
public function getCalculatedStatusAttribute(): string
{
// 判定離線
if ($this->status === 'offline') {
return 'offline';
}
// 判定異常 (檢查過去 15 分鐘內是否有 error 日誌)
$hasRecentErrors = $this->logs()
->where('level', 'error')
->where('created_at', '>=', now()->subMinutes(15))
->exists();
if ($hasRecentErrors) {
return 'error';
}
return 'online';
}
/**
* Scope: 判定在線 (讀取 status 欄位)
*/
public function scopeOnline($query)
{
return $query->where('status', 'online');
}
/**
* Scope: 判定離線 (讀取 status 欄位)
*/
public function scopeOffline($query)
{
return $query->where('status', 'offline');
}
/**
* Scope: 判定異常 (過去 15 分鐘內有錯誤或警告日誌)
*/
public function scopeHasError($query)
{
return $query->whereExists(function ($q) {
$q->select(\Illuminate\Support\Facades\DB::raw(1))
->from('machine_logs')
->whereColumn('machine_logs.machine_id', 'machines.id')
->whereIn('level', ['error', 'warning'])
->where('created_at', '>=', now()->subMinutes(15));
});
}
/**
* Scope: 判定運行中 (在線且無近期異常)
*/
public function scopeRunning($query)
{
return $query->online()->whereNotExists(function ($q) {
$q->select(\Illuminate\Support\Facades\DB::raw(1))
->from('machine_logs')
->whereColumn('machine_logs.machine_id', 'machines.id')
->whereIn('level', ['error', 'warning'])
->where('created_at', '>=', now()->subMinutes(15));
});
}
/**
* Scope: 判定異常在線 (在線且有近期異常)
*/
public function scopeErrorOnline($query)
{
return $query->online()->hasError();
}
protected $casts = [
'last_heartbeat_at' => 'datetime',
'temperature' => 'integer',
'welcome_gift_enabled' => 'boolean',
'is_spring_slot_1_10' => 'boolean',
'is_spring_slot_11_20' => 'boolean',
'is_spring_slot_21_30' => 'boolean',
'is_spring_slot_31_40' => 'boolean',
'is_spring_slot_41_50' => 'boolean',
'is_spring_slot_51_60' => 'boolean',
'member_system_enabled' => 'boolean',
'tax_invoice_enabled' => 'boolean',
'card_terminal_enabled' => 'boolean',
'scan_pay_esun_enabled' => 'boolean',
'scan_pay_linepay_enabled' => 'boolean',
'shopping_cart_enabled' => 'boolean',
'cash_module_enabled' => 'boolean',
'images' => 'array',
];
/**
* Get machine images absolute URLs
*/
public function getImageUrlsAttribute(): array
{
if (empty($this->images)) {
return [];
}
return array_map(fn($path) => \Illuminate\Support\Facades\Storage::disk('public')->url($path), $this->images);
}
public function logs()
{
return $this->hasMany(MachineLog::class);
}
public function slots()
{
return $this->hasMany(MachineSlot::class);
}
public function commands()
{
return $this->hasMany(RemoteCommand::class);
}
public function machineModel()
{
return $this->belongsTo(MachineModel::class);
}
public function paymentConfig()
{
return $this->belongsTo(\App\Models\System\PaymentConfig::class);
}
public function creator()
{
return $this->belongsTo(\App\Models\System\User::class, 'creator_id');
}
public function updater()
{
return $this->belongsTo(\App\Models\System\User::class, 'updater_id');
}
public const PAGE_STATUSES = [
'0' => 'Offline',
'1' => 'Home Page',
'2' => 'Vending Page',
'3' => 'Admin Page',
'4' => 'Replenishment Page',
'5' => 'Tutorial Page',
'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',
];
public function getCurrentPageLabelAttribute(): string
{
$code = (string) $this->current_page;
$label = self::PAGE_STATUSES[$code] ?? $code;
return __($label);
}
public function users()
{
return $this->belongsToMany(\App\Models\System\User::class);
}
}