257 lines
11 KiB
PHP
257 lines
11 KiB
PHP
<?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 = 561586; // cyan-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);
|
||
}
|
||
|
||
$title = '⚠️ ' . $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;
|
||
}
|
||
}
|
||
}
|