star-cloud/app/Models/Machine/MachineLog.php
sky121113 df3c72e927 [FEAT] 機台刷卡機 (Nexsys 終端機) 健康狀態燈號與失敗日誌
1. 刷卡機共用判定:信用卡(1)、電子票證(2)、手機支付(10) 走同一台實體刷卡機,以交易結果維護燈號(刷卡機無斷線心跳)。
2. ProcessTransactionFinalized 新增 syncCardTerminalStatus:成功交易自動消除該機台未解決的 card_terminal 警告;失敗交易解析 payment_response 的 "| code=XXXX" 回應碼,寫一筆 type='card_terminal' 的 warning 日誌(warning 級不觸發 Discord)。
3. MachineService 新增 CARD_TERMINAL_CODE_MAP(NCCC/聯卡中心回應碼對照,值為英文翻譯鍵);MachineLog 依 card_code 重組多語可讀失敗原因。
4. Machine 模型新增 card_terminal_status 與 latest_card_terminal_log_time 取值器;MachineController 列表以 withCount + 相關子查詢預載彙總,避免 N+1。
5. 機台列表頁新增「刷卡機」狀態燈號(綠/琥珀/紅 + 最近異常時間)與「刷卡機日誌」分頁。
6. 文件更新:config/api-docs.php 與 api-technical-specs SKILL.md 補充 payment_type 說明(2/10 與 1 共用同一台刷卡機、補列 42 取物單與 100 遠端出貨)。
7. 既有 migration change_products_spec_to_text 加 sqlite 守衛,讓測試環境可跑(VARCHAR/TEXT 在 sqlite 等價且不支援 MODIFY)。
8. 新增 CardTerminalStatusTest 功能測試(18 斷言通過,涵蓋成功消警、失敗寫日誌、掃碼排除、多語訊息、N+1 預載路徑)。
9. 新增 25 組三語系 key(刷卡機狀態與回應碼),zh_TW/en/ja 對齊並 ksort 排序。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:09:05 +08:00

131 lines
4.6 KiB
PHP
Raw Permalink 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;
class MachineLog extends Model
{
use HasFactory;
const UPDATED_AT = null;
protected static function booted()
{
static::created(function (MachineLog $log) {
// 1. 取得機台與公司,若無公司則直接 return
$machine = $log->machine;
if (!$machine || !$machine->company) {
return;
}
$company = $machine->company;
$companySettings = $company->settings ?? [];
// 必須啟用 Discord 通知功能
if (!($companySettings['discord_notify_enabled'] ?? false)) {
return;
}
// 優先讀取機台專屬 Webhook無則繼承公司全域 Webhook
$machineSettings = $machine->settings ?? [];
$webhookUrl = $machineSettings['discord_webhook_url'] ?? $companySettings['discord_webhook_url'] ?? null;
if (empty($webhookUrl)) {
return;
}
// 2. 判定是否符合發送條件
$shouldNotify = false;
if ($log->type === 'temperature') {
$shouldNotify = true;
} elseif ($log->type === 'status') {
if ($log->message === 'Connection lost (LWT)') {
$shouldNotify = true;
} elseif ($log->message === 'Connection restored') {
// 尋找此機台前一筆狀態日誌 (ID 比當前小type 為 status倒序第一筆)
$previousStatus = self::withoutGlobalScopes()
->where('machine_id', $log->machine_id)
->where('type', 'status')
->where('id', '<', $log->id)
->orderBy('id', 'desc')
->first();
if ($previousStatus && $previousStatus->message === 'Connection lost (LWT)' && $previousStatus->is_resolved) {
// 只有當前一筆是真實 LWT 斷線,且已被消警 (is_resolved = true) 時,才發送恢復通知
$shouldNotify = true;
}
}
} elseif ($log->type === 'submachine') {
// 下位機日誌僅在 warning 或 error 級別時發送 Discord 告警info 狀態日誌不發送
if (in_array($log->level, ['warning', 'error'])) {
$shouldNotify = true;
}
} elseif ($log->level === 'error') {
$shouldNotify = true;
}
// 3. 若符合發送條件,調度非同步任務
if ($shouldNotify) {
\App\Jobs\Notification\SendDiscordNotificationJob::dispatch($log->id);
}
});
}
protected $fillable = [
'company_id',
'machine_id',
'level',
'type',
'message',
'context',
'is_resolved',
];
protected $casts = [
'context' => 'array',
'is_resolved' => 'boolean',
];
protected $appends = [
'translated_message',
];
/**
* 動態重組翻譯後的訊息
*/
public function getTranslatedMessageAttribute(): string
{
$context = $this->context;
// 刷卡機回應碼:用目前代碼表重組失敗原因,支援多語系顯示。
if (isset($context['card_code'])) {
$code = $context['card_code'];
$reason = \App\Services\Machine\MachineService::CARD_TERMINAL_CODE_MAP[$code] ?? 'Transaction failed';
return __('Card payment failed') . '' . __($reason) . " (Code: {$code})";
}
// B013 封裝:優先用目前代碼表重組,讓舊日誌也能吃到最新文案。
if (isset($context['translated_label'])) {
$code = $context['raw_code'] ?? $context['error_code'] ?? '0000';
$mapping = \App\Services\Machine\MachineService::ERROR_CODE_MAP[$code] ?? null;
$label = __($mapping['label'] ?? $context['translated_label']);
$tid = $context['tid'] ?? null;
if ($tid) {
return __('Slot') . " {$tid}: {$label} (Code: {$code})";
}
return "{$label} (Code: {$code})";
}
// 預設退回原始 message (支援歷史資料的翻譯判定與佔位符替換)
return __($this->message, $context ?? []);
}
public function machine()
{
return $this->belongsTo(Machine::class);
}
}