1. 實作倉儲管理系統 (Warehouse Management):包含總覽、庫存、調撥、機台庫存與補貨模組。 2. 建立倉儲相關資料結構:新增 warehouses, warehouse_stocks, stock_in_orders, stock_movements, transfer_orders, replenishment_orders 等資料表。 3. 優化商品管理 (Product Management) 介面:調整商品與類別管理的分頁、搜尋與編輯邏輯,並導入極簡奢華風 UI。 4. 增強遠端機台管理 (Remote Machine Management):優化機台清單、庫存監控與遠端出貨介面,提升載入效能。 5. 更新全站導覽選單 (Sidebar):整合倉儲管理入口,並修復側邊欄語法錯誤。 6. 標準化分頁組件 (Pagination):套用 luxury 奢華風分頁樣式。 7. 補齊多語系語系檔 (i18n):更新 zh_TW.json,包含倉儲與商品模組相關詞彙。
263 lines
7.6 KiB
PHP
263 lines
7.6 KiB
PHP
<?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',
|
||
'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',
|
||
];
|
||
|
||
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',
|
||
'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);
|
||
}
|
||
}
|