- #17: Amélioration gestion des secteurs et statistiques - #18: Optimisation services API et logs - #19: Corrections Flutter widgets et repositories - #20: Fix création passage - détection automatique ope_users.id vs users.id Suppression dossier web/ (migration vers app Flutter) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
457 lines
13 KiB
PHP
Executable File
457 lines
13 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Script CRON pour agrégation des statistiques d'événements
|
|
*
|
|
* Parse les fichiers JSONL et agrège les données dans event_stats_daily
|
|
*
|
|
* Usage:
|
|
* php aggregate_event_stats.php # Agrège J-1
|
|
* php aggregate_event_stats.php --date=2025-12-20 # Agrège une date spécifique
|
|
* php aggregate_event_stats.php --from=2025-12-01 --to=2025-12-21 # Rattrapage plage
|
|
*
|
|
* À exécuter quotidiennement via crontab (1h du matin) :
|
|
* 0 1 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/aggregate_event_stats.php >> /var/www/geosector/api/logs/aggregate_stats.log 2>&1
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
// Configuration
|
|
define('LOCK_FILE', '/tmp/aggregate_event_stats.lock');
|
|
define('EVENT_LOG_DIR', __DIR__ . '/../../logs/events');
|
|
|
|
// Empêcher l'exécution multiple simultanée
|
|
if (file_exists(LOCK_FILE)) {
|
|
$lockTime = filemtime(LOCK_FILE);
|
|
if (time() - $lockTime > 3600) {
|
|
unlink(LOCK_FILE);
|
|
} else {
|
|
die("[" . date('Y-m-d H:i:s') . "] Le processus est déjà en cours d'exécution\n");
|
|
}
|
|
}
|
|
|
|
file_put_contents(LOCK_FILE, (string) getmypid());
|
|
|
|
register_shutdown_function(function () {
|
|
if (file_exists(LOCK_FILE)) {
|
|
unlink(LOCK_FILE);
|
|
}
|
|
});
|
|
|
|
// Simuler l'environnement web pour AppConfig en CLI
|
|
if (php_sapi_name() === 'cli') {
|
|
$hostname = gethostname();
|
|
if (strpos($hostname, 'pra') !== false) {
|
|
$_SERVER['SERVER_NAME'] = 'app3.geosector.fr';
|
|
} elseif (strpos($hostname, 'rca') !== false) {
|
|
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
|
|
} else {
|
|
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr';
|
|
}
|
|
|
|
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
|
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
|
|
if (!function_exists('getallheaders')) {
|
|
function getallheaders()
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Chargement de l'environnement
|
|
require_once __DIR__ . '/../../vendor/autoload.php';
|
|
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
|
require_once __DIR__ . '/../../src/Core/Database.php';
|
|
require_once __DIR__ . '/../../src/Services/LogService.php';
|
|
|
|
use App\Services\LogService;
|
|
|
|
/**
|
|
* Parse les arguments CLI
|
|
*/
|
|
function parseArgs(array $argv): array
|
|
{
|
|
$args = [
|
|
'date' => null,
|
|
'from' => null,
|
|
'to' => null,
|
|
];
|
|
|
|
foreach ($argv as $arg) {
|
|
if (strpos($arg, '--date=') === 0) {
|
|
$args['date'] = substr($arg, 7);
|
|
} elseif (strpos($arg, '--from=') === 0) {
|
|
$args['from'] = substr($arg, 7);
|
|
} elseif (strpos($arg, '--to=') === 0) {
|
|
$args['to'] = substr($arg, 5);
|
|
}
|
|
}
|
|
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Génère la liste des dates à traiter
|
|
*/
|
|
function getDatesToProcess(array $args): array
|
|
{
|
|
$dates = [];
|
|
|
|
if ($args['date']) {
|
|
$dates[] = $args['date'];
|
|
} elseif ($args['from'] && $args['to']) {
|
|
$current = new DateTime($args['from']);
|
|
$end = new DateTime($args['to']);
|
|
while ($current <= $end) {
|
|
$dates[] = $current->format('Y-m-d');
|
|
$current->modify('+1 day');
|
|
}
|
|
} else {
|
|
// Par défaut : J-1
|
|
$dates[] = date('Y-m-d', strtotime('-1 day'));
|
|
}
|
|
|
|
return $dates;
|
|
}
|
|
|
|
/**
|
|
* Parse un fichier JSONL et retourne les événements
|
|
*/
|
|
function parseJsonlFile(string $filePath): array
|
|
{
|
|
$events = [];
|
|
|
|
if (!file_exists($filePath)) {
|
|
return $events;
|
|
}
|
|
|
|
$handle = fopen($filePath, 'r');
|
|
if (!$handle) {
|
|
return $events;
|
|
}
|
|
|
|
while (($line = fgets($handle)) !== false) {
|
|
$line = trim($line);
|
|
if (empty($line)) {
|
|
continue;
|
|
}
|
|
|
|
$event = json_decode($line, true);
|
|
if ($event && isset($event['event'])) {
|
|
$events[] = $event;
|
|
}
|
|
}
|
|
|
|
fclose($handle);
|
|
return $events;
|
|
}
|
|
|
|
/**
|
|
* Agrège les événements par entity_id et event_type
|
|
*/
|
|
function aggregateEvents(array $events): array
|
|
{
|
|
$stats = [];
|
|
|
|
foreach ($events as $event) {
|
|
$entityId = $event['entity_id'] ?? null;
|
|
$eventType = $event['event'] ?? 'unknown';
|
|
$userId = $event['user_id'] ?? null;
|
|
|
|
// Clé d'agrégation : entity_id peut être NULL (stats globales)
|
|
$key = ($entityId ?? 'NULL') . '|' . $eventType;
|
|
|
|
if (!isset($stats[$key])) {
|
|
$stats[$key] = [
|
|
'entity_id' => $entityId,
|
|
'event_type' => $eventType,
|
|
'count' => 0,
|
|
'sum_amount' => 0.0,
|
|
'user_ids' => [],
|
|
'metadata' => [],
|
|
];
|
|
}
|
|
|
|
$stats[$key]['count']++;
|
|
|
|
// Collecter les user_ids pour unique_users
|
|
if ($userId !== null) {
|
|
$stats[$key]['user_ids'][$userId] = true;
|
|
}
|
|
|
|
// Somme des montants pour les passages et paiements Stripe
|
|
if (in_array($eventType, ['passage_created', 'passage_updated'])) {
|
|
$amount = $event['amount'] ?? 0;
|
|
$stats[$key]['sum_amount'] += (float) $amount;
|
|
} elseif (in_array($eventType, ['stripe_payment_success', 'stripe_payment_created'])) {
|
|
// Montant en centimes -> euros
|
|
$amount = ($event['amount'] ?? 0) / 100;
|
|
$stats[$key]['sum_amount'] += (float) $amount;
|
|
}
|
|
|
|
// Collecter metadata spécifiques
|
|
collectMetadata($stats[$key], $event);
|
|
}
|
|
|
|
// Convertir user_ids en count
|
|
foreach ($stats as &$stat) {
|
|
$stat['unique_users'] = count($stat['user_ids']);
|
|
unset($stat['user_ids']);
|
|
|
|
// Finaliser les metadata
|
|
$stat['metadata'] = finalizeMetadata($stat['metadata'], $stat['event_type']);
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* Collecte les métadonnées spécifiques par type d'événement
|
|
*/
|
|
function collectMetadata(array &$stat, array $event): void
|
|
{
|
|
$eventType = $event['event'] ?? '';
|
|
|
|
switch ($eventType) {
|
|
case 'login_failed':
|
|
$reason = $event['reason'] ?? 'unknown';
|
|
$stat['metadata']['reasons'][$reason] = ($stat['metadata']['reasons'][$reason] ?? 0) + 1;
|
|
break;
|
|
|
|
case 'passage_created':
|
|
$sectorId = $event['sector_id'] ?? null;
|
|
if ($sectorId) {
|
|
$stat['metadata']['sectors'][$sectorId] = ($stat['metadata']['sectors'][$sectorId] ?? 0) + 1;
|
|
}
|
|
$paymentType = $event['payment_type'] ?? 'unknown';
|
|
$stat['metadata']['payment_types'][$paymentType] = ($stat['metadata']['payment_types'][$paymentType] ?? 0) + 1;
|
|
break;
|
|
|
|
case 'stripe_payment_failed':
|
|
$errorCode = $event['error_code'] ?? 'unknown';
|
|
$stat['metadata']['error_codes'][$errorCode] = ($stat['metadata']['error_codes'][$errorCode] ?? 0) + 1;
|
|
break;
|
|
|
|
case 'stripe_terminal_error':
|
|
$errorCode = $event['error_code'] ?? 'unknown';
|
|
$stat['metadata']['error_codes'][$errorCode] = ($stat['metadata']['error_codes'][$errorCode] ?? 0) + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finalise les métadonnées (top 5, tri, etc.)
|
|
*/
|
|
function finalizeMetadata(array $metadata, string $eventType): ?array
|
|
{
|
|
if (empty($metadata)) {
|
|
return null;
|
|
}
|
|
|
|
$result = [];
|
|
|
|
// Top 5 secteurs
|
|
if (isset($metadata['sectors'])) {
|
|
arsort($metadata['sectors']);
|
|
$result['top_sectors'] = array_slice($metadata['sectors'], 0, 5, true);
|
|
}
|
|
|
|
// Raisons d'échec login
|
|
if (isset($metadata['reasons'])) {
|
|
arsort($metadata['reasons']);
|
|
$result['failure_reasons'] = $metadata['reasons'];
|
|
}
|
|
|
|
// Types de paiement
|
|
if (isset($metadata['payment_types'])) {
|
|
arsort($metadata['payment_types']);
|
|
$result['payment_types'] = $metadata['payment_types'];
|
|
}
|
|
|
|
// Codes d'erreur
|
|
if (isset($metadata['error_codes'])) {
|
|
arsort($metadata['error_codes']);
|
|
$result['error_codes'] = $metadata['error_codes'];
|
|
}
|
|
|
|
return empty($result) ? null : $result;
|
|
}
|
|
|
|
/**
|
|
* Insère ou met à jour les stats dans la base de données
|
|
*/
|
|
function upsertStats(PDO $db, string $date, array $stats): int
|
|
{
|
|
$upsertedCount = 0;
|
|
|
|
$sql = "
|
|
INSERT INTO event_stats_daily
|
|
(stat_date, entity_id, event_type, count, sum_amount, unique_users, metadata)
|
|
VALUES
|
|
(:stat_date, :entity_id, :event_type, :count, :sum_amount, :unique_users, :metadata)
|
|
ON DUPLICATE KEY UPDATE
|
|
count = VALUES(count),
|
|
sum_amount = VALUES(sum_amount),
|
|
unique_users = VALUES(unique_users),
|
|
metadata = VALUES(metadata),
|
|
updated_at = CURRENT_TIMESTAMP
|
|
";
|
|
|
|
$stmt = $db->prepare($sql);
|
|
|
|
foreach ($stats as $stat) {
|
|
try {
|
|
$stmt->execute([
|
|
'stat_date' => $date,
|
|
'entity_id' => $stat['entity_id'],
|
|
'event_type' => $stat['event_type'],
|
|
'count' => $stat['count'],
|
|
'sum_amount' => $stat['sum_amount'],
|
|
'unique_users' => $stat['unique_users'],
|
|
'metadata' => $stat['metadata'] ? json_encode($stat['metadata'], JSON_UNESCAPED_UNICODE) : null,
|
|
]);
|
|
$upsertedCount++;
|
|
} catch (PDOException $e) {
|
|
echo " ERREUR insertion {$stat['event_type']}: " . $e->getMessage() . "\n";
|
|
}
|
|
}
|
|
|
|
return $upsertedCount;
|
|
}
|
|
|
|
/**
|
|
* Génère également les stats globales (entity_id = NULL)
|
|
*/
|
|
function generateGlobalStats(array $statsByEntity): array
|
|
{
|
|
$globalStats = [];
|
|
|
|
foreach ($statsByEntity as $stat) {
|
|
$eventType = $stat['event_type'];
|
|
|
|
if (!isset($globalStats[$eventType])) {
|
|
$globalStats[$eventType] = [
|
|
'entity_id' => null,
|
|
'event_type' => $eventType,
|
|
'count' => 0,
|
|
'sum_amount' => 0.0,
|
|
'unique_users' => 0,
|
|
'metadata' => null,
|
|
];
|
|
}
|
|
|
|
$globalStats[$eventType]['count'] += $stat['count'];
|
|
$globalStats[$eventType]['sum_amount'] += $stat['sum_amount'];
|
|
$globalStats[$eventType]['unique_users'] += $stat['unique_users'];
|
|
}
|
|
|
|
return array_values($globalStats);
|
|
}
|
|
|
|
// ============================================================
|
|
// MAIN
|
|
// ============================================================
|
|
|
|
try {
|
|
echo "[" . date('Y-m-d H:i:s') . "] Démarrage de l'agrégation des statistiques\n";
|
|
|
|
// Initialisation
|
|
$appConfig = AppConfig::getInstance();
|
|
$config = $appConfig->getFullConfig();
|
|
$environment = $appConfig->getEnvironment();
|
|
|
|
echo "Environnement: {$environment}\n";
|
|
|
|
Database::init($config['database']);
|
|
$db = Database::getInstance();
|
|
|
|
// Parser les arguments
|
|
$args = parseArgs($argv);
|
|
$dates = getDatesToProcess($args);
|
|
|
|
echo "Dates à traiter: " . implode(', ', $dates) . "\n\n";
|
|
|
|
$totalStats = 0;
|
|
$totalEvents = 0;
|
|
|
|
foreach ($dates as $date) {
|
|
$jsonlFile = EVENT_LOG_DIR . '/' . $date . '.jsonl';
|
|
|
|
echo "--- Traitement de {$date} ---\n";
|
|
|
|
if (!file_exists($jsonlFile)) {
|
|
echo " Fichier non trouvé: {$jsonlFile}\n";
|
|
continue;
|
|
}
|
|
|
|
$fileSize = filesize($jsonlFile);
|
|
echo " Fichier: " . basename($jsonlFile) . " (" . number_format($fileSize / 1024, 2) . " KB)\n";
|
|
|
|
// Parser le fichier
|
|
$events = parseJsonlFile($jsonlFile);
|
|
$eventCount = count($events);
|
|
echo " Événements parsés: {$eventCount}\n";
|
|
|
|
if ($eventCount === 0) {
|
|
echo " Aucun événement à agréger\n";
|
|
continue;
|
|
}
|
|
|
|
$totalEvents += $eventCount;
|
|
|
|
// Agréger par entity/event_type
|
|
$stats = aggregateEvents($events);
|
|
echo " Agrégations par entité: " . count($stats) . "\n";
|
|
|
|
// Générer les stats globales
|
|
$globalStats = generateGlobalStats($stats);
|
|
echo " Agrégations globales: " . count($globalStats) . "\n";
|
|
|
|
// Fusionner stats entités + globales
|
|
$allStats = array_merge(array_values($stats), $globalStats);
|
|
|
|
// Insérer en base
|
|
$upserted = upsertStats($db, $date, $allStats);
|
|
echo " Stats insérées/mises à jour: {$upserted}\n";
|
|
|
|
$totalStats += $upserted;
|
|
}
|
|
|
|
// Résumé
|
|
echo "\n=== RÉSUMÉ ===\n";
|
|
echo "Dates traitées: " . count($dates) . "\n";
|
|
echo "Événements traités: {$totalEvents}\n";
|
|
echo "Stats agrégées: {$totalStats}\n";
|
|
|
|
// Log
|
|
LogService::log('Agrégation des statistiques terminée', [
|
|
'level' => 'info',
|
|
'script' => 'aggregate_event_stats.php',
|
|
'environment' => $environment,
|
|
'dates_count' => count($dates),
|
|
'events_count' => $totalEvents,
|
|
'stats_count' => $totalStats,
|
|
]);
|
|
|
|
echo "\n[" . date('Y-m-d H:i:s') . "] Agrégation terminée avec succès\n";
|
|
|
|
} catch (Exception $e) {
|
|
$errorMsg = 'Erreur lors de l\'agrégation: ' . $e->getMessage();
|
|
|
|
LogService::log($errorMsg, [
|
|
'level' => 'error',
|
|
'script' => 'aggregate_event_stats.php',
|
|
'trace' => $e->getTraceAsString(),
|
|
]);
|
|
|
|
echo "\n❌ ERREUR: {$errorMsg}\n";
|
|
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
|
|
|
|
exit(1);
|
|
}
|
|
|
|
exit(0);
|