This commit is contained in:
baodev0
2026-07-21 08:34:15 +07:00
parent 3f477a559d
commit c3b5417b19
19 changed files with 652 additions and 20 deletions
@@ -450,13 +450,20 @@ class CustomerController extends Controller
'password' => Hash::make($password),
]);
\Illuminate\Support\Facades\Mail::to($customer->email)->send(
new \App\Mail\Customer\PasswordResetByAdmin([
'fullname' => $customer->fullname,
'email' => $customer->email,
'password' => $password,
])
);
$passwordMail = new \App\Mail\Customer\PasswordResetByAdmin([
'fullname' => $customer->fullname,
'email' => $customer->email,
'password' => $password,
]);
\Illuminate\Support\Facades\Mail::to($customer->email)->send($passwordMail);
\App\Models\Mail::create([
'subject' => $passwordMail->envelope()->subject,
'to' => $customer->email,
'customer_id' => $customer->id,
'message' => $passwordMail->render(),
]);
Log::create([
'agent' => 'admin',
@@ -558,6 +565,15 @@ class CustomerController extends Controller
$mailer->send(new ManualCustomerEmail($subject, $message));
\App\Models\Mail::create([
'template_id' => $template->id ?? 0,
'subject' => $subject,
'to' => implode(', ', $to),
'cc' => implode(', ', $cc),
'customer_id' => $customer->id,
'message' => $message,
]);
$admin = Auth::guard('admin')->user();
Log::create([
'agent' => 'admin',
@@ -15,7 +15,7 @@ final class EmailLogController extends Controller
{
$limit = min((int) $request->get('limit', $request->get('per_page', 20)), 200);
$query = Mail::query()->orderByDesc('created_at');
$query = Mail::query()->with(['customer', 'template'])->orderByDesc('created_at');
// sort=field_asc|field_desc
if ($sort = $request->get('sort')) {
@@ -30,7 +30,7 @@ final class EmailLogController extends Controller
}
if ($status = $request->get('status')) {
$query->where('status', $status);
$query->where('status', array_search($status, Mail::STATUS_MAP, true) ?: $status);
}
if ($customerId = $request->get('customer_id')) {
$query->where('customer_id', (int) $customerId);
@@ -47,7 +47,7 @@ final class EmailLogController extends Controller
return response()->json([
'error' => false,
'data' => [
'items' => $paginator->items(),
'items' => $paginator->getCollection()->map($this->mapLog(...))->all(),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
@@ -58,12 +58,28 @@ final class EmailLogController extends Controller
public function show(int $id): JsonResponse
{
$log = Mail::find($id);
$log = Mail::with(['customer', 'template'])->find($id);
if (!$log) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy email log'], 404);
}
return response()->json(['error' => false, 'data' => $log]);
return response()->json(['error' => false, 'data' => $this->mapLog($log)]);
}
private function mapLog(Mail $log): array
{
return [
'id' => $log->id,
'to_email' => $log->to,
'to_name' => $log->customer->fullname ?? null,
'customer_id' => $log->customer_id,
'subject' => $log->subject,
'template' => $log->template->name ?? '—',
'status' => $log->status,
'error_message' => null,
'sent_at' => $log->status === 'sent' ? $log->created_at : null,
'created_at' => $log->created_at,
];
}
}
@@ -13,22 +13,68 @@ class NotificationController extends Controller
{
$recipient = $this->resolveRecipient($recipientType);
$notifications = Notification::query()
$query = Notification::query()
->where('recipient_type', $recipientType)
->where('recipient_id', $recipient->id)
->orderByDesc('created_at')
->limit(20)
->get();
->where('recipient_id', $recipient->id);
$unreadCount = (clone $query)->where('is_read', false)->count();
if ($request->filled('is_read')) {
$query->where('is_read', $request->boolean('is_read'));
}
if ($request->filled('search')) {
$search = $request->string('search');
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('body', 'like', "%{$search}%");
});
}
$query->orderByDesc('created_at');
if ($request->filled('page') || $request->filled('per_page')) {
$perPage = (int) $request->integer('per_page', 15);
$paginator = $query->paginate($perPage, ['*'], 'page', (int) $request->integer('page', 1));
return response()->json([
'error' => false,
'data' => [
'items' => $paginator->items(),
'total' => $paginator->total(),
'unread_count' => $unreadCount,
],
]);
}
$notifications = $query->limit(20)->get();
return response()->json([
'error' => false,
'data' => [
'items' => $notifications,
'unread_count' => $notifications->where('is_read', false)->count(),
'unread_count' => $unreadCount,
],
]);
}
public function show(Request $request, string $recipientType, int $id): JsonResponse
{
$recipient = $this->resolveRecipient($recipientType);
$notification = Notification::query()
->where('recipient_type', $recipientType)
->where('recipient_id', $recipient->id)
->whereKey($id)
->first();
if (!$notification) {
return response()->json(['error' => true, 'message' => 'Notification not found'], 404);
}
return response()->json(['error' => false, 'data' => $notification]);
}
public function markRead(Request $request, string $recipientType, int $id): JsonResponse
{
$recipient = $this->resolveRecipient($recipientType);
@@ -661,6 +661,77 @@ class VCenterManagerController extends Controller
], 400);
}
public function stats(Request $request)
{
$vmId = (int) $request->input('vm_id');
$vm = VirtualMachines::find($vmId);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
}
$vmwareId = $vm->vm_id;
if (!$vmwareId) {
return response()->json(['error' => true, 'message' => 'VM VMware ID not found'], 400);
}
if ($vm->power_state !== 'POWERED_ON') {
return response()->json(['error' => true, 'message' => 'VM is not powered on'], 400);
}
$vcenter = $vm->datacenter?->vCenter ?? $vm->host?->datacenter?->vCenter;
if (!$vcenter) {
return response()->json(['error' => true, 'message' => 'vCenter not found'], 400);
}
$vCenterService = new \App\Http\Services\vCenter();
if (!$vCenterService->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
}
$result = $vCenterService->GetVMStats($vmwareId);
if ($result['error'] ?? true) {
return response()->json([
'error' => true,
'message' => $result['message'] ?? 'Failed to fetch VM stats',
], 400);
}
$stats = $result['data'];
$cpuPercent = $stats['cpu']['used_percent'] ?? 0;
$memPercent = $stats['memory']['usage_percent'] ?? 0;
// ponytail: overall_status is a CPU/RAM usage threshold heuristic, not real vCenter
// health/alarms — that needs the SOAP EventManager/AlarmManager, not wired up in this
// codebase (REST Automation API only). Upgrade path: add a SOAP client and read
// triggeredAlarmState off the VM's ManagedEntity if per-alarm detail is ever needed.
$overallStatus = match (true) {
$cpuPercent >= 90 || $memPercent >= 90 => 'red',
$cpuPercent >= 75 || $memPercent >= 75 => 'yellow',
default => 'green',
};
return response()->json([
'error' => false,
'data' => [
'overall_status' => $overallStatus,
'cpu_usage_percent' => round($cpuPercent, 1),
'mem_usage_percent' => round($memPercent, 1),
'mem_usage_mb' => $stats['memory']['used_MiB'] ?? 0,
'mem_total_mb' => $stats['memory']['size_MiB'] ?? 0,
// ponytail: no uptime source wired up (would need SOAP PerfManager
// sys.uptime.latest); frontend renders "—" when null.
'uptime_seconds' => null,
'alarms' => [],
],
]);
}
public function getPoweredOffVms()
{
$vms = VirtualMachines::whereNotNull('vm_id')
+19
View File
@@ -161,6 +161,25 @@ class Telegram
return $this->SendMessage($msg);
}
/**
* Notify one step of the VM rebuild pipeline (success or failure).
*/
public function SendRebuildStepNotification(string $vmName, string $ip, string $step, bool $success, ?string $note = null)
{
$msg = ($success ? '🔄' : '❌') . " Rebuild VPS — {$step}\n";
$msg .= "--------------------------------\n";
$msg .= "🖥️ VM: {$vmName}\n";
$msg .= "🌐 IP: {$ip}\n";
$msg .= "📊 Trạng thái: " . ($success ? 'Hoàn tất ✅' : 'Thất bại ❌') . "\n";
if ($note) {
$msg .= "📝 " . ($success ? 'Ghi chú' : 'Lỗi') . ": {$note}\n";
}
$msg .= "🕒 Thời gian: " . Carbon::now()->format('d/m/Y H:i:s') . "\n";
$msg .= "--------------------------------";
return $this->SendMessage($msg);
}
/**
* Send a photo to Telegram group
* @param string $filePath Local storage path (e.g. Upload/2024/06/01/image.png)
@@ -0,0 +1,40 @@
<?php
namespace App\Jobs\Mail\Customer;
use App\Mail\Customer\VmRebuildFailed;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class VmRebuildFailedJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function handle(): void
{
if (empty($this->data['customer_email'])) {
return;
}
try {
Mail::to($this->data['customer_email'])
->send(new VmRebuildFailed($this->data));
} catch (\Throwable $e) {
\Log::error('Không thể gửi email thông báo rebuild thất bại: ' . $e->getMessage(), [
'customer_email' => $this->data['customer_email'],
'module_vmware_id' => $this->data['module_vmware_id'] ?? null,
]);
}
}
}
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace App\Jobs\VirtualMachine\Rebuild;
use App\Http\Services\Telegram;
use App\Jobs\Mail\Customer\VmRebuildFailedJob;
use App\Models\ModuleVmware;
use App\Services\VirtualMachine\Rebuild\VmRebuildAddonService;
use App\Services\VirtualMachine\Rebuild\VmRebuildService;
use Illuminate\Bus\Queueable;
@@ -62,6 +65,8 @@ final class RebuildAddonInstallJob implements ShouldQueue
'module_vmware_id' => $this->moduleVmwareId,
'new_vm_id' => $this->newVmId,
]);
(new Telegram)->SendRebuildStepNotification($ctx->vmName, $ctx->ip, 'Cài đặt addon (CPU/RAM/Disk)', true);
}
public function failed(Throwable $e): void
@@ -77,5 +82,22 @@ final class RebuildAddonInstallJob implements ShouldQueue
'error_message' => 'AddonInstall failed: ' . $e->getMessage(),
'updated_at' => now(),
]);
$vm = ModuleVmware::with('customer')->find($this->moduleVmwareId);
(new Telegram)->SendRebuildStepNotification(
$vm->name ?? "VM #{$this->moduleVmwareId}",
$vm->ip ?? '',
'Cài đặt addon (CPU/RAM/Disk)',
false,
$e->getMessage(),
);
VmRebuildFailedJob::dispatch([
'customer_email' => $vm?->customer?->email,
'customer_name' => $vm?->customer?->fullname,
'vm_name' => $vm->name ?? "VM #{$this->moduleVmwareId}",
'ip' => $vm->ip ?? '',
'module_vmware_id' => $this->moduleVmwareId,
]);
}
}
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace App\Jobs\VirtualMachine\Rebuild;
use App\Http\Services\Telegram;
use App\Jobs\Mail\Customer\VmRebuildFailedJob;
use App\Models\ModuleVmware;
use App\Services\VirtualMachine\Rebuild\VmRebuildIpService;
use App\Services\VirtualMachine\Rebuild\VmRebuildService;
use Illuminate\Bus\Queueable;
@@ -73,6 +76,8 @@ final class RebuildAssignIpJob implements ShouldQueue
'module_vmware_id' => $this->moduleVmwareId,
'new_vm_id' => $this->newVmId,
]);
(new Telegram)->SendRebuildStepNotification($ctx->vmName, $ctx->ip, 'Gán địa chỉ IP', true);
}
public function failed(Throwable $e): void
@@ -88,5 +93,22 @@ final class RebuildAssignIpJob implements ShouldQueue
'error_message' => 'AssignIp failed: ' . $e->getMessage(),
'updated_at' => now(),
]);
$vm = ModuleVmware::with('customer')->find($this->moduleVmwareId);
(new Telegram)->SendRebuildStepNotification(
$vm->name ?? "VM #{$this->moduleVmwareId}",
$vm->ip ?? '',
'Gán địa chỉ IP',
false,
$e->getMessage(),
);
VmRebuildFailedJob::dispatch([
'customer_email' => $vm?->customer?->email,
'customer_name' => $vm?->customer?->fullname,
'vm_name' => $vm->name ?? "VM #{$this->moduleVmwareId}",
'ip' => $vm->ip ?? '',
'module_vmware_id' => $this->moduleVmwareId,
]);
}
}
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace App\Jobs\VirtualMachine\Rebuild;
use App\Http\Services\Telegram;
use App\Jobs\Mail\Customer\VmRebuildFailedJob;
use App\Models\ModuleVmware;
use App\Services\VirtualMachine\Rebuild\VmRebuildPasswordService;
use App\Services\VirtualMachine\Rebuild\VmRebuildService;
use Illuminate\Bus\Queueable;
@@ -62,6 +65,15 @@ final class RebuildChangePasswordJob implements ShouldQueue
'module_vmware_id' => $this->moduleVmwareId,
'new_vm_id' => $this->newVmId,
]);
$vmAfter = ModuleVmware::find($this->moduleVmwareId);
(new Telegram)->SendRebuildStepNotification(
$ctx->vmName,
$ctx->ip,
'Đổi mật khẩu' . ($ctx->isWindows ? ' & Port RDP' : ''),
true,
$ctx->isWindows ? "Port RDP mới: {$vmAfter?->port}" : null,
);
}
public function failed(Throwable $e): void
@@ -77,5 +89,22 @@ final class RebuildChangePasswordJob implements ShouldQueue
'error_message' => 'ChangePassword failed: ' . $e->getMessage(),
'updated_at' => now(),
]);
$vm = ModuleVmware::with('customer')->find($this->moduleVmwareId);
(new Telegram)->SendRebuildStepNotification(
$vm->name ?? "VM #{$this->moduleVmwareId}",
$vm->ip ?? '',
'Đổi mật khẩu & Port',
false,
$e->getMessage(),
);
VmRebuildFailedJob::dispatch([
'customer_email' => $vm?->customer?->email,
'customer_name' => $vm?->customer?->fullname,
'vm_name' => $vm->name ?? "VM #{$this->moduleVmwareId}",
'ip' => $vm->ip ?? '',
'module_vmware_id' => $this->moduleVmwareId,
]);
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Jobs\VirtualMachine\Rebuild;
use App\Http\Services\Telegram;
use App\Http\Services\vCenter as VCenterService;
use App\Models\ModuleVmware;
use App\Models\vCenter as VCenterModel;
@@ -82,6 +83,8 @@ final class RebuildDeleteOldVmJob implements ShouldQueue
'old_vm_id' => $this->oldVmId,
'result' => $res,
]);
(new Telegram)->SendRebuildStepNotification($vm->name ?? '', $vm->ip ?? '', 'Xoá VM cũ', true);
} catch (Throwable $e) {
Log::error('RebuildDeleteOldVmJob: delete failed', [
'old_vm_id' => $this->oldVmId,
@@ -99,5 +102,14 @@ final class RebuildDeleteOldVmJob implements ShouldQueue
'old_vm_id' => $this->oldVmId,
'error' => $e->getMessage(),
]);
$vm = ModuleVmware::find($this->moduleVmwareId);
(new Telegram)->SendRebuildStepNotification(
$vm->name ?? "VM #{$this->moduleVmwareId}",
$vm->ip ?? '',
'Xoá VM cũ',
false,
"Old VM {$this->oldVmId} cần xoá thủ công: " . $e->getMessage(),
);
}
}
@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace App\Jobs\VirtualMachine\Rebuild;
use App\Http\Services\Telegram;
use App\Jobs\Mail\Admin\AdminOrderNotificationJob;
use App\Jobs\Mail\Customer\VmRebuildFailedJob;
use App\Jobs\Mail\SendInformationVirtualMachine;
use App\Models\Invoice;
use App\Models\ModuleVmware;
@@ -94,6 +96,8 @@ final class RebuildFinalizeJob implements ShouldQueue
'new_vm_id' => $this->newVmId,
'old_vm_id' => $oldVmId,
]);
(new Telegram)->SendRebuildStepNotification($ctx->vmName, $ctx->ip, 'Hoàn tất & chuyển đổi VM', true, 'Rebuild thành công, VM mới đã online.');
}
public function failed(Throwable $e): void
@@ -109,6 +113,23 @@ final class RebuildFinalizeJob implements ShouldQueue
'error_message' => $e->getMessage(),
'updated_at' => now(),
]);
$vm = ModuleVmware::with('customer')->find($this->moduleVmwareId);
(new Telegram)->SendRebuildStepNotification(
$vm->name ?? "VM #{$this->moduleVmwareId}",
$vm->ip ?? '',
'Hoàn tất & chuyển đổi VM',
false,
$e->getMessage(),
);
VmRebuildFailedJob::dispatch([
'customer_email' => $vm?->customer?->email,
'customer_name' => $vm?->customer?->fullname,
'vm_name' => $vm->name ?? "VM #{$this->moduleVmwareId}",
'ip' => $vm->ip ?? '',
'module_vmware_id' => $this->moduleVmwareId,
]);
}
private function sendCredentialsEmail(int $moduleVmwareId): void
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Jobs\VirtualMachine\Rebuild;
use App\Http\Services\Telegram;
use App\Jobs\Mail\Customer\VmRebuildFailedJob;
use App\Models\ModuleVmware;
use App\Models\ReinstallLog;
use App\Models\Service;
@@ -39,6 +41,8 @@ final class RebuildVirtualMachineJob implements ShouldQueue
public int $tries = 1;
public int $timeout = 900;
private bool $failureHandled = false;
public function __construct(
public readonly int $moduleVmwareId,
public readonly int $newOsId,
@@ -85,6 +89,8 @@ final class RebuildVirtualMachineJob implements ShouldQueue
'module_vmware_id' => $this->moduleVmwareId,
'new_vm_id' => $newVmId,
]);
(new Telegram)->SendRebuildStepNotification($ctx->vmName, $ctx->ip, 'Clone VM mới', true);
} catch (Throwable $e) {
$this->onFailure($e);
throw $e;
@@ -98,6 +104,11 @@ final class RebuildVirtualMachineJob implements ShouldQueue
private function onFailure(Throwable $e): void
{
if ($this->failureHandled) {
return;
}
$this->failureHandled = true;
Log::error('RebuildVirtualMachineJob: failed — old VM preserved', [
'module_vmware_id' => $this->moduleVmwareId,
'error' => $e->getMessage(),
@@ -113,6 +124,23 @@ final class RebuildVirtualMachineJob implements ShouldQueue
'status' => ReinstallLog::STATUS_FAILED,
'error_message' => $e->getMessage(),
]);
$vm = ModuleVmware::with('customer')->find($this->moduleVmwareId);
(new Telegram)->SendRebuildStepNotification(
$vm->name ?? "VM #{$this->moduleVmwareId}",
$vm->ip ?? '',
'Clone VM mới',
false,
$e->getMessage(),
);
VmRebuildFailedJob::dispatch([
'customer_email' => $vm?->customer?->email,
'customer_name' => $vm?->customer?->fullname,
'vm_name' => $vm->name ?? "VM #{$this->moduleVmwareId}",
'ip' => $vm->ip ?? '',
'module_vmware_id' => $this->moduleVmwareId,
]);
}
private function markServiceStatus(?int $serviceId, string $status): void
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Mail\Customer;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class VmRebuildFailed extends Mailable
{
use Queueable, SerializesModels;
public array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Thông báo: Cài lại hệ điều hành thất bại - ' . ($this->data['vm_name'] ?? 'VPS'),
);
}
public function content(): Content
{
return new Content(
view: 'emails.customer.vm-rebuild-failed',
with: [
'customerName' => $this->data['customer_name'] ?? 'Khách hàng',
'vmName' => $this->data['vm_name'] ?? 'N/A',
'ip' => $this->data['ip'] ?? 'N/A',
'supportEmail' => config('mail.support_email', 'support@example.com'),
'supportPhone' => config('mail.support_phone', '1900-xxxx'),
'serviceUrl' => rtrim((string) env('FRONTEND_URL', config('app.url')), '/') . '/dich-vu',
],
);
}
public function attachments(): array
{
return [];
}
}
+22
View File
@@ -20,4 +20,26 @@ class Mail extends Model
'message',
'status'
];
// ponytail: status is stored as tinyint (1=sent, 2=failed, 3=pending) but the
// admin API/UI contract expects string enums; map it here so every reader agrees.
public const STATUS_MAP = [1 => 'sent', 2 => 'failed', 3 => 'pending'];
protected function status(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn ($value) => self::STATUS_MAP[$value] ?? self::STATUS_MAP[1],
set: fn ($value) => is_string($value) ? (array_search($value, self::STATUS_MAP, true) ?: 1) : $value,
);
}
public function customer()
{
return $this->belongsTo(Customer::class, 'customer_id');
}
public function template()
{
return $this->belongsTo(Template::class, 'template_id');
}
}
@@ -6,6 +6,7 @@ namespace App\Services\VirtualMachine\Rebuild;
use App\DTOs\VirtualMachine\Rebuild\RebuildContext;
use App\Http\Facade\SignRSA;
use App\Http\Services\vCenter as VCenterService;
use App\Models\ModuleVmware;
use App\Models\ReinstallLog;
use App\Models\vCenter as VCenterModel;
@@ -25,8 +26,10 @@ use RuntimeException;
*/
final class VmRebuildPasswordService
{
public function __construct(private readonly VsphereHostSshService $ssh)
{
public function __construct(
private readonly VsphereHostSshService $ssh,
private readonly VCenterService $vCenter,
) {
}
/**
@@ -97,9 +100,44 @@ final class VmRebuildPasswordService
'reinstall_log_id' => $log?->id,
]);
// Windows RDP port is rotated together with the password — a fresh
// random port each rebuild. Linux keeps port 22 (no remote SSH-port
// change script exists on the jump host yet).
if ($ctx->isWindows) {
$this->rotatePort($ctx, $newPassword);
}
return $newPassword;
}
private function rotatePort(RebuildContext $ctx, string $guestPassword): void
{
$newPort = (string) random_int(10000, 60000);
$result = $this->vCenter->changeRDPPortWindows(
$ctx->transientName,
$ctx->guestUser,
$guestPassword,
$newPort,
);
$status = is_array($result) ? strtolower((string) ($result['status'] ?? '')) : '';
if ($status !== 'ok') {
throw new RuntimeException(
'Rebuild port: changeRDPPortWindows failed — ' . (is_string($result) ? $result : json_encode($result)),
);
}
ModuleVmware::where('id', $ctx->moduleVmwareId)->update([
'port' => (int) ($result['newPort'] ?? $newPort),
]);
Log::info('VmRebuildPasswordService: RDP port rotated', [
'module_vmware_id' => $ctx->moduleVmwareId,
'new_port' => $result['newPort'] ?? $newPort,
]);
}
private function generatePassword(int $length): string
{
$sets = [
@@ -6,9 +6,12 @@ namespace App\Services\VirtualMachine\Rebuild;
use App\DTOs\VirtualMachine\Rebuild\RebuildContext;
use App\Http\Facade\SignRSA;
use App\Http\Services\Telegram;
use App\Jobs\VirtualMachine\Rebuild\RebuildVirtualMachineJob;
use App\Models\Customer;
use App\Models\ModuleHost;
use App\Models\ModuleVmware;
use App\Models\Notification;
use App\Models\Os;
use App\Models\ReinstallLog;
use App\Models\Service;
@@ -189,6 +192,28 @@ final class VmRebuildService
RebuildVirtualMachineJob::dispatch($ctx->moduleVmwareId, $ctx->newOsId, $log->id)
->onQueue('default');
if ($initiator === 'client') {
$customerName = Customer::find($initiatorCustomerId)?->fullname ?? "Khách hàng #{$initiatorCustomerId}";
Notification::notifyAdmins([
'type' => 'vps_rebuild_requested',
'title' => 'Khách hàng vừa yêu cầu rebuild VPS',
'body' => "{$customerName} vừa rebuild VPS {$ctx->vmName} ({$ctx->ip})",
'link' => '/rebuild-logs',
'data' => [
'module_vmware_id' => $ctx->moduleVmwareId,
'reinstall_log_id' => $log->id,
],
]);
}
(new Telegram)->SendRebuildStepNotification(
$ctx->vmName,
$ctx->ip,
'Bắt đầu rebuild',
true,
"Khởi tạo bởi: {$initiator}",
);
return $log;
}
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
DB::table('mail')->where('customer_id', 0)->update(['customer_id' => null]);
// ponytail: raw SQL instead of Blueprint::change() to avoid adding doctrine/dbal just for this
DB::statement('ALTER TABLE `mail` MODIFY `customer_id` BIGINT UNSIGNED NULL DEFAULT NULL');
Schema::table('mail', function (Blueprint $table) {
$table->foreign('customer_id')->references('id')->on('customers')->nullOnDelete();
});
}
public function down()
{
Schema::table('mail', function (Blueprint $table) {
$table->dropForeign(['customer_id']);
});
DB::statement('ALTER TABLE `mail` MODIFY `customer_id` BIGINT NOT NULL DEFAULT 0');
}
};
@@ -0,0 +1,121 @@
@php
$appName = $appName ?? config('app.name', 'Thế Giới Số');
$fullName = $customerName ?? 'Quý khách';
$vmName = $vmName ?? 'N/A';
$ip = $ip ?? 'N/A';
$serviceUrl = $serviceUrl ?? rtrim((string) env('FRONTEND_URL', config('app.url')), '/') . '/dich-vu';
$supportEmail = $supportEmail ?? 'support@tgs.com.vn';
$supportPhone = $supportPhone ?? '1900. 6119';
@endphp
<table style="border-collapse: collapse; width: 100%; background-color: #efefef;" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td style="padding: 20px;" align="center" valign="top">
<table style="padding-top: 20px; padding-bottom: 20px; border-collapse: collapse; background-color: #ffffff; width: 700px;" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr class="NoiDungKM">
<td style="width: 700px;" valign="top">
<table style="border-collapse: collapse; width: 100%;" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td style="background-color: #f06824; padding: 34px 30px 30px; text-align: center; font-family: Arial, Helvetica, sans-serif; color: #ffffff;" align="center" valign="top" bgcolor="#f06824">
<div style="font-size: 42px; line-height: 1; margin-bottom: 12px;">⚠️</div>
<h1 style="margin: 0 0 8px; padding: 0; color: #ffffff; font-family: Arial, Helvetica, sans-serif; font-size: 24px; line-height: 1.35; font-weight: 700;">
Thông báo về VPS {{ $vmName }}
</h1>
<div style="font-size: 14px; line-height: 1.6; color: #ffffff;">
Đội ngũ kỹ thuật đang xử lý yêu cầu của Quý khách
</div>
</td>
</tr>
<tr class="MXH">
<td style="text-align: center; padding: 0 30px;" align="center" valign="top">
<table style="border-collapse: collapse; background-color: #ffffff; width: 100%;" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td style="height: 15px; font-family: Arial, Helvetica, sans-serif; font-size: 14px; line-height: 1.7; color: #000000;" valign="top" height="15">
<div style="text-align: left;">&nbsp;</div>
<div style="text-align: left;">Xin chào <strong>{{ $fullName }}</strong>,</div>
<div style="text-align: left;">&nbsp;</div>
<div style="text-align: left;">
Rất tiếc, yêu cầu cài lại hệ điều hành (rebuild) cho VPS của Quý khách hiện chưa thực hiện thành công. Đội ngũ kỹ thuật của <strong>{{ $appName }}</strong> đã được thông báo để kiểm tra và xử lý.
</div>
<div style="text-align: left;">&nbsp;</div>
<div style="text-align: left;"><strong>Thông tin dịch vụ:</strong></div>
<div style="text-align: left;">&nbsp;&nbsp;&nbsp;&nbsp;+ Tên VPS: <strong>{{ $vmName }}</strong></div>
<div style="text-align: left;">&nbsp;&nbsp;&nbsp;&nbsp;+ IP: <strong>{{ $ip }}</strong></div>
<div style="text-align: left;">&nbsp;</div>
<div style="text-align: left;">
VPS hiện tại của Quý khách <strong>vẫn đang hoạt động bình thường</strong> trên cấu hình cũ, không có dữ liệu hay dịch vụ nào bị gián đoạn. Quý khách không cần thực hiện thêm thao tác nào, chúng tôi sẽ liên hệ lại khi rebuild được xử lý xong.
</div>
<div style="text-align: left;">&nbsp;</div>
<div style="text-align: center;">
<a style="display: inline-block; background-color: #f06824; color: #ffffff !important; text-decoration: none; padding: 12px 28px; border-radius: 3px; font-family: Arial, Helvetica, sans-serif; font-size: 14px; font-weight: 700;" href="{{ $serviceUrl }}" target="_blank" rel="noopener">
Xem dịch vụ của bạn
</a>
</div>
<div style="text-align: left;">&nbsp;</div>
<div style="text-align: left;">
Nếu cần hỗ trợ khẩn cấp, Quý khách vui lòng liên hệ qua email <strong>{{ $supportEmail }}</strong> hoặc tổng đài <strong>{{ $supportPhone }}</strong>.
</div>
<div style="text-align: left;">&nbsp;</div>
<div style="text-align: left; color: #666666; font-size: 13px;">
Email này được gửi tự động. Vui lòng không trả lời email này.
</div>
<div style="text-align: left;">&nbsp;</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr class="ThongTin">
<td align="center" valign="top" bgcolor="#065d88">
<table style="border-collapse: collapse; background-color: #f06824; border-top: 0; border-bottom: 0; width: 100%;" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr style="height: 30px;">
<td style="height: 30px;" valign="top" height="10">&nbsp;</td>
</tr>
<tr style="height: 148px;">
<td style="padding: 0; font-family: Helvetica, Arial, sans-serif; text-decoration: none; color: #e7e7e7; font-size: 16px; font-weight: normal; line-height: 150%; vertical-align: top; text-align: center; height: 148px;" align="center">
<p style="margin: 0; padding: 0; font-family: Helvetica, Arial, sans-serif; color: white; font-size: 11pt; line-height: 200%;">
<span style="color: #ffffff; font-family: 'times new roman', times, serif;"><strong style="font-size: 20px;">CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ</strong></span><br>
<span style="color: #ffffff; font-family: arial, helvetica, sans-serif; font-size: 12pt;">Địa chỉ: 128/38 Tân Hương, Phường Phú Thọ Hòa, Thành Phố Hồ Chí Minh</span><br>
<span style="color: #ffffff; font-family: arial, helvetica, sans-serif; font-size: 12pt;">Tel: (028) 7309. 7379 - Tổng đài: {{ $supportPhone }}</span><br>
<span style="font-family: arial, helvetica, sans-serif; font-size: 12pt;">Email: <span style="color: #ffffff;"><a style="color: #ffffff; text-decoration: none;" href="mailto:{{ $supportEmail }}">{{ $supportEmail }}</a></span></span><br>
<span style="text-decoration: none; font-family: arial, helvetica, sans-serif; font-size: 12pt; color: #ffffff !important;">
<a style="color: #ffffff !important; text-decoration: none;" href="https://www.tgs.com.vn">www.TGS.com.vn</a> &amp;
<a style="color: #ffffff !important; text-decoration: none;" href="https://www.thegioiso.cloud">www.thegioiso.CLOUD</a> &amp;
<a style="color: #ffffff !important; text-decoration: none;" href="https://www.thegioiserver.com">www.thegioiSERVER.com</a> &amp;
<a style="color: #ffffff !important; text-decoration: none;" href="https://www.cloudemail.vn">www.CloudEmail.vn</a>
</span>
</p>
<p style="margin: 0; padding: 0; font-family: Helvetica, Arial, sans-serif; color: white; font-size: 11pt; line-height: 200%;">
<span style="text-decoration: none; font-family: 'times new roman', times, serif; color: #ffffff !important; font-size: 12pt;">
<span style="color: #ffffff; font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt;">
<span style="text-decoration: none; font-family: 'times new roman', times, serif; color: #ffffff !important;">
<span style="font-family: arial, sans-serif;"><strong>🥇Thế Giới Số🥇</strong>- Premium HOSTING • CLOUD • SERVER &amp; WEBSITE | 15+ năm Phụng Sự 🚀</span>
</span>
</span>
</span>
</p>
</td>
</tr>
<tr>
<td style="height: 30px; color: #ffffff;" valign="top" height="30"><span style="font-size: 14px; color: #ffffff;">&nbsp;</span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
+5
View File
@@ -598,6 +598,7 @@ Route::prefix('v1')
Route::post('vm/suspend', 'suspend')->middleware('perm:module-vcenter.edit');
Route::post('vm/update-config', 'updateConfig')->middleware('perm:module-vcenter.edit');
Route::post('vm/delete', 'deleteVM')->middleware('perm:module-vcenter.edit');
Route::get('vm/stats', 'stats')->middleware('perm:module-vcenter.view');
Route::get('vms/powered-off', 'getPoweredOffVms')->middleware('perm:module-vcenter.view');
});
@@ -615,6 +616,10 @@ Route::prefix('v1')
->controller(NotificationController::class)
->group(function () {
Route::get('/', fn() => app(NotificationController::class)->index(request(), 'admin'));
Route::get(
'{id}',
fn($id) => app(NotificationController::class)->show(request(), 'admin', (int) $id),
)->whereNumber('id');
Route::patch(
'{id}/read',
fn($id) => app(NotificationController::class)->markRead(request(), 'admin', (int) $id),