Enhance VM service management: add service_id and plain_password fields, sync service records, and update related migrations

This commit is contained in:
pbao9
2026-04-16 09:39:52 +07:00
parent bb73d6c5a2
commit d68fda2cf0
11 changed files with 206 additions and 104 deletions
@@ -20,7 +20,7 @@ class ServiceClientDetailResource extends JsonResource
$orderItem = $this->orderItem;
$order = $orderItem?->order;
$vm = $order?->vmWare?->first();
$vm = $this->resource->vmware;
$product = $orderItem?->product;
return [
@@ -52,9 +52,11 @@ class ServiceClientDetailResource extends JsonResource
'ip_address' => $vm->ipAddress?->ip,
'hostname' => $vm->hostname,
'username' => $vm->user,
'password' => $vm->getRawOriginal('password'),
'password' => $vm->plain_password ?? null,
'port' => $vm->port,
'status' => $vm->status,
'ip' => $vm->ip ?? null,
'dns' => $vm->dns ?? null,
] : null,
];
}
@@ -43,7 +43,10 @@ class ConfigCLICommand implements ShouldQueue
try {
$vm = \App\Models\ModuleVmware::where('vmware_id', $this->vmware_id)->first();
if ($vm) {
$vm->update(['password' => \App\Http\Facade\SignRSA::EncryptFunc($PasswordVirtualMachine)]);
$vm->update([
'password' => \App\Http\Facade\SignRSA::EncryptFunc($PasswordVirtualMachine),
'plain_password' => $PasswordVirtualMachine,
]);
}
} catch (\Exception $e) {
Log::error('❌ Lỗi khi lưu password: ' . $e->getMessage());
@@ -14,6 +14,7 @@ use App\Models\ModuleVmware;
use App\Models\ModuleIpAddress;
use App\Models\IpPool;
use App\Models\Log as CustomerLog;
use App\Models\Service;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -231,28 +232,33 @@ class CreateVirtualMachineJob implements ShouldQueue
$firstPayment = (int) $this->item->price * ($months ?: 1);
$nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null;
$initialPassword = 'TGS@' . \Illuminate\Support\Str::random(8);
$moduleVmware = ModuleVmware::create([
'name' => $serverName,
'module_ip_address' => $moduleIpAddress->id,
'port' => 22,
'order_id' => $this->item->order->id,
'first_payment' => $firstPayment,
'next_payment' => $firstPayment,
'next_due_date' => $nextDueDate,
'next_invoice' => $nextDueDate,
'user' => 'administrator',
'password' => 'TGS@123',
'hostname' => $serverName,
'vmware_id' => $vmId,
'vcenter_id' => $vcenter->id,
'os_id' => $this->item->os_id ?? 1,
'customer_id' => $customer->id,
'status' => 1,
'name' => $serverName,
'module_ip_address' => $moduleIpAddress->id,
'port' => 22,
'order_id' => $this->item->order->id,
'first_payment' => $firstPayment,
'next_payment' => $firstPayment,
'next_due_date' => $nextDueDate,
'next_invoice' => $nextDueDate,
'user' => 'administrator',
'password' => $initialPassword,
'plain_password' => $initialPassword,
'hostname' => $serverName,
'vmware_id' => $vmId,
'vcenter_id' => $vcenter->id,
'os_id' => $this->item->os_id ?? 1,
'customer_id' => $customer->id,
'status' => 1,
]);
// Explicit sync: update services table directly (belt + suspenders vs observer)
$this->syncServiceRecord($moduleVmware);
AssignIPJob::dispatch($vmId, $moduleVmware->id, $moduleIpAddress->ip)
->delay(now()->addMinutes(5));
// Service record sẽ được tạo tự động bởi ModuleVmwareObserver::created()
} catch (\Exception $e) {
Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [
'vmware_id' => $vmId,
@@ -459,4 +465,43 @@ class CreateVirtualMachineJob implements ShouldQueue
Log::error('Không thể gửi email thông báo lỗi VM: ' . $e->getMessage());
}
}
/**
* Sync module_vmware info → services table.
* Link: module_vmware.order_id → order_items.order_id → services.order_item_id
*/
private function syncServiceRecord(ModuleVmware $moduleVmware): void
{
try {
$orderItem = OrderItem::where('order_id', $moduleVmware->order_id)->first();
if (!$orderItem) {
Log::warning('syncServiceRecord: không tìm thấy OrderItem', [
'order_id' => $moduleVmware->order_id,
'module_vmware_id' => $moduleVmware->id,
]);
return;
}
$months = (int) ($orderItem->months ?? 0);
$updated = Service::where('order_item_id', $orderItem->id)
->update([
'vm_id' => $moduleVmware->id,
'status' => 'active',
'expires_at' => $months > 0 ? now()->addMonths($months) : null,
]);
Log::info('syncServiceRecord: service synced', [
'module_vmware_id' => $moduleVmware->id,
'vmware_id' => $moduleVmware->vmware_id,
'order_item_id' => $orderItem->id,
'rows_updated' => $updated,
]);
} catch (\Throwable $e) {
Log::error('syncServiceRecord: exception — ' . $e->getMessage(), [
'module_vmware_id' => $moduleVmware->id,
'order_id' => $moduleVmware->order_id,
]);
}
}
}
+11 -1
View File
@@ -27,7 +27,11 @@ class ModuleVmware extends Model
'vcenter_id',
'os_id',
'customer_id',
'status'
'status',
'service_id',
'ip',
'dns',
'plain_password',
];
// NOTE: password must NOT use 'hashed' cast — it is a VM guest credential
@@ -63,6 +67,12 @@ class ModuleVmware extends Model
return $this->hasMany(\App\Models\Service::class, 'vm_id', 'vmware_id');
}
/** Direct link via service_id FK (new). */
public function service()
{
return $this->belongsTo(\App\Models\Service::class, 'service_id');
}
public function getStatus()
{
return match ($this->status) {
+2 -2
View File
@@ -61,10 +61,10 @@ class Service extends Model
return $this->hasMany(ServiceAddon::class, 'service_id');
}
/** The VPS/VM associated with this service (if applicable). */
/** The VPS/VM associated with this service — FK: services.vm_id → module_vmware.id */
public function vmware()
{
return $this->belongsTo(ModuleVmware::class, 'vm_id', 'vmware_id');
return $this->belongsTo(ModuleVmware::class, 'vm_id');
}
// -------------------------------------------------------------------------
+18 -21
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Observers;
use App\Models\Invoice;
use App\Models\ModuleVmware;
use App\Models\OrderItem;
use App\Models\Service;
@@ -101,35 +100,33 @@ class ModuleVmwareObserver
{
try {
$orderItem = OrderItem::where('order_id', $moduleVmware->order_id)
->where('os_id', $moduleVmware->os_id)
->first();
$invoice = Invoice::where('order_id', $moduleVmware->order_id)->first();
if (!$orderItem) {
Log::warning('ModuleVmwareObserver: không tìm thấy OrderItem', [
'order_id' => $moduleVmware->order_id,
'module_vmware_id' => $moduleVmware->id,
]);
return;
}
Service::firstOrCreate(
['order_item_id' => $orderItem?->id],
[
'customer_id' => $moduleVmware->customer_id,
'product_id' => $orderItem?->product_id,
'invoice_id' => $invoice?->id,
'vm_id' => $moduleVmware->vmware_id,
'server_name' => $moduleVmware->hostname ?? $moduleVmware->name,
'status' => 'active',
'auto_renew' => false,
'started_at' => now(),
'expires_at' => ($orderItem && (int) $orderItem->months > 0)
? now()->addMonths((int) $orderItem->months)
: null,
]
);
$updated = Service::where('order_item_id', $orderItem->id)
->update([
'vm_id' => $moduleVmware->id,
'status' => 'active',
'expires_at' => (int) $orderItem->months > 0
? now()->addMonths((int) $orderItem->months)
: null,
]);
Log::info('Service record synced for ModuleVmware', [
'module_vmware_id' => $moduleVmware->id,
'vmware_id' => $moduleVmware->vmware_id,
'order_item_id' => $orderItem?->id,
'order_item_id' => $orderItem->id,
'rows_updated' => $updated,
]);
} catch (\Throwable $e) {
Log::error('Lỗi khi tạo Service từ ModuleVmwareObserver: ' . $e->getMessage(), [
Log::error('Lỗi khi sync Service từ ModuleVmwareObserver: ' . $e->getMessage(), [
'module_vmware_id' => $moduleVmware->id,
'order_id' => $moduleVmware->order_id,
]);
@@ -13,14 +13,14 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient
}
/**
* Lấy danh sách sản phẩm đang hoạt động
* Lấy toàn bộ dịch vụ của customer (mọi trạng thái)
*/
public function getActiveServices(int $limit = 10)
{
return $this->model
->with(['orderItem' => fn($q) => $q->with('product')])
->where('status', 'active')
->orderBy('started_at', 'asc')
->orderByRaw("FIELD(status, 'active', 'pending', 'suspended', 'expired', 'cancelled', 'terminated')")
->orderBy('started_at', 'desc')
->paginate($limit);
}
@@ -50,7 +50,8 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient
->with([
'orderItem.os.osGroup',
'orderItem.product',
'orderItem.order.vmWare.ipAddress',
'orderItem.order',
'vmware.ipAddress',
])
->where('uuid', $uuid)
->first();
+6 -56
View File
@@ -4,14 +4,11 @@ namespace App\Services\Api\Order;
use App\Enums\Order\OrderStatus;
use App\Helpers\OrderHelper;
use App\Jobs\VirtualMachine\Create\CreateVMJobs;
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
use App\Models\Addon;
use App\Models\Log as CustomerActivityLog;
use App\Models\ModuleIpAddress;
use App\Models\ModuleVmware;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Os;
use App\Models\Service;
use App\Models\ServiceAddon;
use App\Repositories\Api\Invoice\InvoiceRepository;
@@ -239,59 +236,12 @@ class OrderService
private function dispatchVmJob(Order $order, OrderItem $item, Service $service, $customer): void
{
try {
$os = Os::with('osGroup.host')->find($item->os_id);
dispatch(new CreateVirtualMachineJob($item));
if (!$os?->osGroup?->module_host_id) {
Log::warning('dispatchVmJob: OS/OsGroup/Host not found', [
'order_id' => $order->id,
'os_id' => $item->os_id,
]);
return;
}
$ip = ModuleIpAddress::where('module_host_id', $os->osGroup->module_host_id)
->where('status', 0)
->lockForUpdate()
->first();
if (!$ip) {
Log::warning('dispatchVmJob: no available IP', [
'order_id' => $order->id,
'module_host_id' => $os->osGroup->module_host_id,
]);
return;
}
$vmName = $item->server_name ?? ('VM-' . $order->id);
$ip->update(['status' => 1, 'vm_name' => $vmName]);
$moduleVmware = ModuleVmware::create([
'name' => $vmName,
'module_ip_address_id' => $ip->id,
'port' => 22,
'order_id' => $order->id,
'first_payment' => $order->total,
'next_payment' => $order->total,
'next_due_date' => $service->expires_at,
'next_invoice' => 0,
'user' => 'Administrator',
'password' => '',
'hostname' => strtolower($vmName),
'vmware_id' => null,
'os_id' => $item->os_id,
'customer_id' => $customer->id,
'status' => 0,
]);
// Store module_vmware.id as vm_id on Service for quick lookup
$service->update(['vm_id' => (string) $moduleVmware->id]);
dispatch(new CreateVMJobs($moduleVmware->id))->delay(now()->addSeconds(10));
Log::info('dispatchVmJob: CreateVMJobs dispatched', [
'order_id' => $order->id,
'service_id' => $service->id,
'module_vmware_id' => $moduleVmware->id,
Log::info('dispatchVmJob: CreateVirtualMachineJob dispatched', [
'order_id' => $order->id,
'service_id' => $service->id,
'order_item_id' => $item->id,
]);
} catch (Exception $e) {
Log::error('dispatchVmJob: exception', [
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('module_vmware', function (Blueprint $table) {
if (!Schema::hasColumn('module_vmware', 'service_id')) {
$table->unsignedBigInteger('service_id')->nullable()->index()->after('id');
$table->foreign('service_id')
->references('id')
->on('services')
->cascadeOnDelete();
}
});
}
public function down(): void
{
Schema::table('module_vmware', function (Blueprint $table) {
if (Schema::hasColumn('module_vmware', 'service_id')) {
$table->dropForeign(['service_id']);
$table->dropColumn('service_id');
}
});
}
};
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('module_vmware', function (Blueprint $table) {
if (!Schema::hasColumn('module_vmware', 'plain_password')) {
$table->text('plain_password')->nullable()->after('password')
->comment('Plaintext VM password — shown to customer on service detail');
}
});
}
public function down(): void
{
Schema::table('module_vmware', function (Blueprint $table) {
if (Schema::hasColumn('module_vmware', 'plain_password')) {
$table->dropColumn('plain_password');
}
});
}
};
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
// Step 1: drop the old string column (loses legacy vmware_id string values)
Schema::table('services', function (Blueprint $table) {
$table->dropColumn('vm_id');
});
// Step 2: add as proper integer FK → module_vmware.id
Schema::table('services', function (Blueprint $table) {
$table->unsignedBigInteger('vm_id')->nullable()->index()->after('invoice_id')
->comment('FK → module_vmware.id (set after VM provisioning completes)');
$table->foreign('vm_id')
->references('id')
->on('module_vmware')
->nullOnDelete();
});
}
public function down(): void
{
Schema::table('services', function (Blueprint $table) {
$table->dropForeign(['vm_id']);
$table->dropColumn('vm_id');
});
Schema::table('services', function (Blueprint $table) {
$table->string('vm_id')->nullable()->index()->after('invoice_id');
});
}
};