laravel reverb

This commit is contained in:
Juan Felipe Zapata Moreno 2026-01-10 08:51:36 -06:00
parent 0487421758
commit 1b4eba2c6c
9 changed files with 377 additions and 24 deletions

View File

@ -0,0 +1,59 @@
<?php
namespace App\Events;
use App\Models\AlertaRobo;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class VehiculoRobadoDetectado implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public AlertaRobo $alerta;
public function __construct(AlertaRobo $alerta)
{
$this->alerta = $alerta;
}
/**
* Canal público para todos los usuarios
*/
public function broadcastOn(): Channel
{
return new Channel('alertas-robos');
}
/**
* Nombre del evento en el frontend
*/
public function broadcastAs(): string
{
return 'vehiculo.robado.detectado';
}
/**
* Datos que se envían al frontend
*/
public function broadcastWith(): array
{
return [
'alerta_id' => $this->alerta->id,
'fast_id' => $this->alerta->fast_id,
'vin' => $this->alerta->vin,
'placa' => $this->alerta->placa,
'marca' => $this->alerta->marca,
'modelo' => $this->alerta->modelo,
'color' => $this->alerta->color,
'arco_id' => $this->alerta->arco_id,
'arco_nombre' => $this->alerta->arco_nombre,
'antena' => $this->alerta->antena,
'fecha_deteccion' => $this->alerta->fecha_deteccion->toIso8601String(),
'mensaje' => "🚨 VEHÍCULO ROBADO DETECTADO: {$this->alerta->placa} en {$this->alerta->arco_nombre}",
];
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\AlertaRobo;
use Illuminate\Http\Request;
use Notsoweb\ApiResponse\Enums\ApiResponse;
class AlertaRoboController extends Controller
{
/**
* Listar alertas pendientes (no vistas)
* GET /api/alertas/pendientes
*/
public function pendientes(Request $request)
{
$alertas = AlertaRobo::pendientes()
->recientes()
->with(['arco', 'usuario'])
->get();
return ApiResponse::OK->response([
'success' => true,
'total' => $alertas->count(),
'alertas' => $alertas
]);
}
/**
* Listar todas las alertas con filtros
* GET /api/alertas
*/
public function index(Request $request)
{
$query = AlertaRobo::query()->with(['arco', 'usuario']);
// Filtro por estado (visto/no visto)
if ($request->has('visto')) {
$query->where('visto', $request->boolean('visto'));
}
// Filtro por arco
if ($request->has('arco_id')) {
$query->where('arco_id', $request->arco_id);
}
// Filtro por placa
if ($request->has('placa') && !empty($request->placa)) {
$query->where('placa', 'like', '%' . $request->placa . '%');
}
// Filtro por VIN
if ($request->has('vin') && !empty($request->vin)) {
$query->where('vin', 'like', '%' . $request->vin . '%');
}
// Filtro por rango de fechas
if ($request->has('fecha_desde')) {
$query->where('fecha_deteccion', '>=', $request->fecha_desde);
}
if ($request->has('fecha_hasta')) {
$query->where('fecha_deteccion', '<=', $request->fecha_hasta);
}
$alertas = $query->orderBy('fecha_deteccion', 'desc')
->paginate($request->input('per_page', 15));
return ApiResponse::OK->response([
'success' => true,
'alertas' => $alertas
]);
}
/**
* Ver una alerta específica
* GET /api/alertas/{id}
*/
public function show(int $id)
{
$alerta = AlertaRobo::with(['arco', 'usuario'])->find($id);
if (!$alerta) {
return ApiResponse::NOT_FOUND->response([
'success' => false,
'message' => 'Alerta no encontrada'
]);
}
return ApiResponse::OK->response([
'success' => true,
'alerta' => $alerta
]);
}
/**
* Confirmar/marcar alerta como vista
* PUT /api/alertas/{id}/confirmar
*/
public function confirmar(Request $request, int $id)
{
$alerta = AlertaRobo::find($id);
if (!$alerta) {
return ApiResponse::NOT_FOUND->response([
'success' => false,
'message' => 'Alerta no encontrada'
]);
}
if ($alerta->visto) {
return ApiResponse::BAD_REQUEST->response([
'success' => false,
'message' => 'Esta alerta ya fue confirmada anteriormente',
'confirmada_por' => $alerta->usuario?->name,
'fecha_confirmacion' => $alerta->fecha_confirmacion
]);
}
// Marcar como vista
$alerta->visto = true;
$alerta->usuario_id = auth()->id(); // Usuario autenticado que confirmó
$alerta->fecha_confirmacion = now();
$alerta->save();
return ApiResponse::OK->response([
'success' => true,
'message' => 'Alerta confirmada exitosamente',
'alerta' => $alerta
]);
}
}

73
app/Models/AlertaRobo.php Normal file
View File

@ -0,0 +1,73 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AlertaRobo extends Model
{
protected $table = 'alertas_robos';
protected $fillable = [
'fast_id',
'vin',
'placa',
'marca',
'modelo',
'color',
'arco_id',
'arco_nombre',
'antena',
'fecha_deteccion',
'visto',
'usuario_id',
'fecha_confirmacion',
];
protected $casts = [
'visto' => 'boolean',
'fecha_deteccion' => 'datetime',
'fecha_confirmacion' => 'datetime',
];
/**
* Relación con el arco
*/
public function arco(): BelongsTo
{
return $this->belongsTo(Arco::class, 'arco_id');
}
/**
* Relación con el usuario que confirmó
*/
public function usuario(): BelongsTo
{
return $this->belongsTo(User::class, 'usuario_id');
}
/**
* Scope para obtener alertas pendientes (no vistas)
*/
public function scopePendientes($query)
{
return $query->where('visto', false);
}
/**
* Scope para obtener alertas vistas
*/
public function scopeVistas($query)
{
return $query->where('visto', true);
}
/**
* Scope para ordenar por más reciente
*/
public function scopeRecientes($query)
{
return $query->orderBy('fecha_deteccion', 'desc');
}
}

View File

@ -2,7 +2,8 @@
namespace App\Services;
use App\Models\Vehicle;
use App\Events\VehiculoRobadoDetectado;
use App\Models\AlertaRobo;
use App\Models\Detection;
use App\Models\Arco;
use Illuminate\Support\Facades\Redis;
@ -27,7 +28,7 @@ public function procesarDeteccion(string $fastId, int $arcoId, ?string $antena =
if ($enRedis) {
// Ya está marcado como robado, verificar si sigue así
$resultado = $this->verificarVehiculoRobado($fastId, json_decode($enRedis, true));
$resultado = $this->verificarVehiculoRobado($fastId, json_decode($enRedis, true), $arcoId, $antena);
$this->registrarDeteccion($fastId, $resultado, $arcoId, $antena);
return $resultado;
}
@ -87,7 +88,6 @@ public function consultarVehiculoPorTag(string $fastId)
'fast_id' => $fastId
]);
return null;
} catch (\Exception $e) {
Log::error('VehicleService: Error en consulta', [
'fast_id' => $fastId,
@ -118,8 +118,6 @@ public function listarVehiculosRobados(): array
/**
* Listar todas las detecciones del día desde Redis
* @param string|null $fecha Fecha en formato Y-m-d (opcional, por defecto hoy)
* @return array
*/
public function listarDeteccionesDelDia(?string $fecha = null): array
{
@ -145,8 +143,6 @@ public function listarDeteccionesDelDia(?string $fecha = null): array
/**
* Obtener estadísticas de detecciones del día
* @param string|null $fecha Fecha en formato Y-m-d (opcional, por defecto hoy)
* @return array
*/
public function obtenerEstadisticasDelDia(?string $fecha = null): array
{
@ -245,19 +241,22 @@ private function consultarNuevoVehiculo(string $fastId): array
}
/**
* Verificar vehículo robado (ya está en Redis)
* Verificar vehículo robado (si ya está en Redis)
*/
private function verificarVehiculoRobado(string $fastId, array $datosRedis): array
private function verificarVehiculoRobado(string $fastId, array $datosRedis, ?int $arcoId = null, ?string $antena = null): array
{
// Actualizar contador de detecciones
$this->actualizarDeteccionRedis($fastId, $datosRedis);
Log::warning('Vehículo robado detectado nuevamente', [
Log::warning('¡VEHÍCULO ROBADO DETECTADO!', [
'fast_id' => $fastId,
'vin' => $datosRedis['vin'] ?? null,
'placa' => $datosRedis['placa'] ?? null,
'detecciones' => ($datosRedis['detecciones'] ?? 0) + 1
'placa' => $datosRedis['placa'] ?? null
]);
// Crear alerta en la base de datos
$this->crearAlertaRobo($fastId, $datosRedis, $arcoId, $antena);
return [
'success' => true,
'tiene_reporte_robo' => true,
@ -267,6 +266,7 @@ private function verificarVehiculoRobado(string $fastId, array $datosRedis): arr
];
}
/**
* Actualizar contador de detecciones en Redis
*/
@ -363,4 +363,38 @@ private function registrarDeteccionDelDia(string $fastId, array $vehiculo, ?int
// Establecer expiración automática al final del día
Redis::expire($key, $segundosHastaFinDelDia);
}
/**
* Crear alerta de vehículo robado y disparar evento
*/
private function crearAlertaRobo(string $fastId, array $datosVehiculo, ?int $arcoId, ?string $antena): void
{
// Obtener nombre del arco
$arcoNombre = $arcoId ? Arco::find($arcoId)?->nombre : 'Desconocido';
// Crear alerta en la base de datos
$alerta = AlertaRobo::create([
'fast_id' => $fastId,
'vin' => $datosVehiculo['vin'] ?? null,
'placa' => $datosVehiculo['placa'] ?? null,
'marca' => $datosVehiculo['marca'] ?? null,
'modelo' => $datosVehiculo['modelo'] ?? null,
'color' => $datosVehiculo['color'] ?? null,
'arco_id' => $arcoId,
'arco_nombre' => $arcoNombre,
'antena' => $antena,
'fecha_deteccion' => now(),
'visto' => false,
]);
// Disparar evento para Laravel Reverb (WebSocket)
event(new VehiculoRobadoDetectado($alerta));
Log::info('Alerta de vehículo robado creada y evento disparado', [
'alerta_id' => $alerta->id,
'fast_id' => $fastId,
'placa' => $alerta->placa,
'arco' => $arcoNombre
]);
}
}

View File

@ -10,7 +10,7 @@
"laravel/framework": "^12.0",
"laravel/passport": "^12.4",
"laravel/pulse": "^1.4",
"laravel/reverb": "^1.4",
"laravel/reverb": "^1.7",
"laravel/tinker": "^2.10",
"notsoweb/laravel-core": "dev-main",
"spatie/laravel-permission": "^6.16",

20
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0cdf26aa072a7f833793cfba72e94e81",
"content-hash": "2aee9315866597be53eadf5c315e8b69",
"packages": [
{
"name": "brick/math",
@ -1855,16 +1855,16 @@
},
{
"name": "laravel/reverb",
"version": "v1.5.0",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/reverb.git",
"reference": "bf84766ad35d9174fb508147f956e8bcf2e46e91"
"reference": "4300fbbc3535f5cbbdf600b00edca1e4c515bfcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/reverb/zipball/bf84766ad35d9174fb508147f956e8bcf2e46e91",
"reference": "bf84766ad35d9174fb508147f956e8bcf2e46e91",
"url": "https://api.github.com/repos/laravel/reverb/zipball/4300fbbc3535f5cbbdf600b00edca1e4c515bfcf",
"reference": "4300fbbc3535f5cbbdf600b00edca1e4c515bfcf",
"shasum": ""
},
"require": {
@ -1884,8 +1884,8 @@
"symfony/http-foundation": "^6.3|^7.0"
},
"require-dev": {
"orchestra/testbench": "^8.0|^9.0|^10.0",
"pestphp/pest": "^2.0|^3.0",
"orchestra/testbench": "^8.36|^9.15|^10.8",
"pestphp/pest": "^2.0|^3.0|^4.0",
"phpstan/phpstan": "^1.10",
"ratchet/pawl": "^0.4.1",
"react/async": "^4.2",
@ -1931,9 +1931,9 @@
],
"support": {
"issues": "https://github.com/laravel/reverb/issues",
"source": "https://github.com/laravel/reverb/tree/v1.5.0"
"source": "https://github.com/laravel/reverb/tree/v1.7.0"
},
"time": "2025-03-31T14:06:47+00:00"
"time": "2026-01-06T16:26:25+00:00"
},
{
"name": "laravel/serializable-closure",
@ -10494,5 +10494,5 @@
"php": "^8.3"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@ -0,0 +1,43 @@
<?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::create('alertas_robos', function (Blueprint $table) {
$table->id();
$table->string('fast_id');
$table->string('vin')->nullable();
$table->string('placa')->nullable();
$table->string('marca')->nullable();
$table->string('modelo')->nullable();
$table->string('color')->nullable();
$table->unsignedBigInteger('arco_id');
$table->string('arco_nombre');
$table->string('antena')->nullable();
$table->timestamp('fecha_deteccion');
$table->boolean('visto')->default(false);
$table->unsignedBigInteger('usuario_id')->nullable();
$table->timestamp('fecha_confirmacion')->nullable();
$table->timestamps();
// Índices para optimizar consultas
$table->index('visto');
$table->index('arco_id');
$table->index('fecha_deteccion');
// Foreign keys
$table->foreign('arco_id')->references('id')->on('arcos')->onDelete('cascade');
$table->foreign('usuario_id')->references('id')->on('users')->onDelete('set null');
});
}
public function down(): void
{
Schema::dropIfExists('alertas_robos');
}
};

View File

@ -13,6 +13,8 @@ services:
- DB_PASSWORD=${DB_PASSWORD}
- DB_DATABASE=${DB_DATABASE}
- DB_PORT=${DB_PORT}
ports:
- "8080:8080"
volumes:
- ./:/var/www/arcos-backend
- ./vendor:/var/www/arcos/vendor

View File

@ -1,5 +1,7 @@
<?php
use App\Http\Controllers\Api\AlertaRoboController;
use App\Http\Controllers\Api\PruebaReverbController;
use App\Http\Controllers\Api\VehicleController;
use App\Http\Controllers\Api\ArcoController;
use Illuminate\Support\Facades\Route;
@ -40,6 +42,13 @@
Route::resource('/arcos', ArcoController::class);
Route::patch('/arcos/{id}/toggle-estado', [ArcoController::class, 'toggleEstado']);
Route::get('/arcos/{id}/detecciones/dia', [ArcoController::class, 'deteccionesDelDia']);
//alerta
Route::get('/alertas', [AlertaRoboController::class, 'index']);
Route::get('/alertas/{id}', [AlertaRoboController::class, 'show']);
Route::get('/alertas/pendientes', [AlertaRoboController::class, 'pendientes']);
Route::put('/alertas/{id}/confirmar', [AlertaRoboController::class, 'confirmar']);
});
/** Rutas públicas */