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>
131 lines
4.6 KiB
PHP
131 lines
4.6 KiB
PHP
<?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);
|
||
}
|
||
}
|