star-cloud/app/Services/Notification/DiscordWebhookService.php
sky121113 acde99e383 [FIX] 修正下位機 Discord 告警發送條件與層級對等
1. 調整 MachineLog 的 created 監聽器,使 submachine 類型日誌僅在 level 為 warning 或 error 時才派發 Discord 通知任務,避免 info 狀態日誌(如出貨中、平台上升中)洗板告警。
2. 優化 DiscordWebhookService 的下位機異常渲染,根據日誌嚴重程度 (level) 動態調整標題前面的 Emoji 圖示,error 級別顯示 🚨,warning 級別顯示 ⚠️
2026-05-28 14:53:06 +08:00

258 lines
11 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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\Services\Notification;
use App\Models\Machine\MachineLog;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class DiscordWebhookService
{
// B013 硬體代碼中文翻譯對照
protected array $errorCodes = [
'0402' => '出貨成功',
'0403' => '貨道卡貨 (重大異常)',
'0202' => '貨道缺貨',
'0412' => '昇降機上升異常',
'0415' => '取貨門異常',
'5402' => '取貨門未關 (警告)',
'5403' => '昇降系統故障',
];
/**
* 發送測試通知
*/
public function sendTestAlert(string $url): bool
{
$payload = [
'username' => 'Star Cloud Alert Bot',
'embeds' => [
[
'title' => '🎉 Star Cloud 測試通知',
'description' => "這是一則來自 Star Cloud 後台的 Discord Webhook 連線測試通知。\n如果您收到這則訊息,代表您的 Webhook 設定已順利生效並正常運作!",
'color' => 561586, // cyan-600 (0x0891B2)
'fields' => [
[
'name' => '測試時間 (台北時間)',
'value' => now()->timezone('Asia/Taipei')->format('Y-m-d H:i:s'),
'inline' => true,
],
[
'name' => '發送人員',
'value' => auth()->user() ? auth()->user()->name : 'System',
'inline' => true,
],
],
'footer' => [
'text' => 'Star Cloud Notification Subsystem',
],
'timestamp' => now()->toISOString(),
]
]
];
return $this->postToWebhook($url, $payload);
}
/**
* 發送機台日誌告警
*/
public function sendMachineLogAlert(string $url, MachineLog $log): bool
{
$machine = $log->machine;
if (!$machine) {
return false;
}
// 1. 智慧語系切換與還原
$company = $machine->company;
$companyLocale = $company ? ($company->settings['locale'] ?? 'zh_TW') : 'zh_TW';
$originalLocale = app()->getLocale();
app()->setLocale($companyLocale);
try {
$title = '';
$description = '';
$color = 561586; // 預設藍色 (cyan-600)
$fields = [];
// 根據日誌類別與內容動態建構卡片
if ($log->type === 'temperature') {
$temp = $log->context['temp'] ?? 'N/A';
$limit = $log->context['limit'] ?? 'N/A';
if (str_contains($log->message, 'too high')) {
$title = '🔴 ' . $this->cleanTranslation(__('Temperature Alert (High)'), $companyLocale);
$description = $this->cleanTranslation(__('Current temperature (:temp°C) is above the upper limit (:limit°C).', ['temp' => $temp, 'limit' => $limit]), $companyLocale);
$color = 14753096; // rose-600
$fields[] = [
'name' => $this->cleanTranslation(__('Current Temperature'), $companyLocale),
'value' => "`{$temp}°C`",
'inline' => true,
];
$fields[] = [
'name' => $this->cleanTranslation(__('Temperature Limit'), $companyLocale),
'value' => "`{$limit}°C`",
'inline' => true,
];
} elseif (str_contains($log->message, 'too low')) {
$title = '🔴 ' . $this->cleanTranslation(__('Temperature Alert (Low)'), $companyLocale);
$description = $this->cleanTranslation(__('Current temperature (:temp°C) is below the lower limit (:limit°C).', ['temp' => $temp, 'limit' => $limit]), $companyLocale);
$color = 14753096; // rose-600 (與過高告警一致使用紅色)
$fields[] = [
'name' => $this->cleanTranslation(__('Current Temperature'), $companyLocale),
'value' => "`{$temp}°C`",
'inline' => true,
];
$fields[] = [
'name' => $this->cleanTranslation(__('Temperature Limit'), $companyLocale),
'value' => "`{$limit}°C`",
'inline' => true,
];
} else {
$title = '🟢 ' . $this->cleanTranslation(__('Temperature Restored'), $companyLocale);
$description = $this->cleanTranslation(__('Current temperature has returned to normal range (:temp°C).', ['temp' => $temp]), $companyLocale);
$color = 366185; // emerald-600
$fields[] = [
'name' => $this->cleanTranslation(__('Current Temperature'), $companyLocale),
'value' => "`{$temp}°C`",
'inline' => true,
];
}
} elseif ($log->type === 'status') {
if ($log->message === 'Connection lost (LWT)') {
$title = '🔴 ' . $this->cleanTranslation(__('Machine Connectivity Lost'), $companyLocale);
$description = $this->cleanTranslation(__('System detected that the machine disconnected unexpectedly.'), $companyLocale);
$color = 14753096; // rose-600 (0xE11D48)
} elseif ($log->message === 'Connection restored') {
$title = '🟢 ' . $this->cleanTranslation(__('Machine Connectivity Restored'), $companyLocale);
$description = $this->cleanTranslation(__('Machine has reconnected to the server and resumed normal heartbeat.'), $companyLocale);
$color = 366185; // emerald-600 (0x059669)
} else {
$title = ' ' . $this->cleanTranslation(__('Machine Status Updated'), $companyLocale);
$description = $this->cleanTranslation(__($log->message), $companyLocale);
$color = 14251782; // warning/amber
}
} elseif ($log->type === 'submachine') {
// 解析 B013 Payload可能包含 tid (貨道) 與 error_code
$data = is_array($log->context) ? $log->context : json_decode($log->context ?? '', true);
$errorCode = $data['error_code'] ?? $data['raw_code'] ?? 'Unknown';
$slotNo = $data['tid'] ?? 'N/A';
// 智慧異常對譯:查詢 MachineService::ERROR_CODE_MAP
$mapping = \App\Services\Machine\MachineService::ERROR_CODE_MAP[$errorCode] ?? null;
if ($mapping) {
$translatedError = __($mapping['label']);
} else {
$translatedError = $data['translated_label'] ?? $errorCode;
$translatedError = __($translatedError);
}
$icon = ($log->level === 'error') ? '🚨 ' : '⚠️ ';
$title = $icon . $this->cleanTranslation(__('Submachine Exception'), $companyLocale);
$description = $this->cleanTranslation(__('Vending machine reported a submachine hardware error.'), $companyLocale);
$color = ($log->level === 'error') ? 14753096 : 14251782;
$fields[] = [
'name' => __('Error Code'),
'value' => "`{$errorCode}`",
'inline' => true,
];
$fields[] = [
'name' => __('Error Details'),
'value' => $this->cleanTranslation($translatedError, $companyLocale),
'inline' => true,
];
$fields[] = [
'name' => __('Affected Slot'),
'value' => "`{$slotNo}`",
'inline' => true,
];
} else {
// 其他類型的日誌
$title = '📢 ' . $this->cleanTranslation(__('Machine Status Updated'), $companyLocale);
$description = $this->cleanTranslation(__($log->message), $companyLocale);
$color = ($log->level === 'error') ? 14753096 : (($log->level === 'warning') ? 14251782 : 561586);
}
// 加入機台基本欄位資訊
$fields[] = [
'name' => __('Machine Name'),
'value' => $machine->name ?? __('Unnamed Machine'),
'inline' => true,
];
$fields[] = [
'name' => __('Serial No'),
'value' => "`{$machine->serial_no}`",
'inline' => true,
];
// 所屬公司欄位依使用者要求完全移除
if ($machine->location) {
$fields[] = [
'name' => __('Location'),
'value' => $machine->location,
'inline' => false,
];
}
$payload = [
'username' => 'Star Cloud Alert Bot',
'embeds' => [
[
'title' => $title,
'description' => $description,
'color' => $color,
'fields' => $fields,
'footer' => [
'text' => 'Star Cloud Telemetry Subsystem',
],
'timestamp' => $log->created_at ? $log->created_at->toISOString() : now()->toISOString(),
]
]
];
return $this->postToWebhook($url, $payload);
} finally {
// 強制還原語系,防止系統語系污染
app()->setLocale($originalLocale);
}
}
/**
* 清理翻譯字串,移除末尾或中間的英文技術括號說明(如 "(K-PDT)"、"(LWT)" 等)
*/
protected function cleanTranslation(string $text, string $locale): string
{
if ($locale === 'en') {
return $text;
}
// 匹配半角及全角括號,內含英數字、連字號、空格等,例如 " (K-PDT)" 或 " LWT"
return preg_replace('/\s*[\(\][A-Za-z0-9\-_\s]+[\)\]/u', '', $text);
}
/**
* HTTP POST 請求 Discord Webhook
*/
protected function postToWebhook(string $url, array $payload): bool
{
try {
$response = Http::timeout(5)->post($url, $payload);
if ($response->successful()) {
return true;
}
Log::error('Discord Webhook Failed: ' . $response->body());
return false;
} catch (\Exception $e) {
Log::error('Discord Webhook Exception: ' . $e->getMessage());
return false;
}
}
}