feat: Version 3.6.2 - Correctifs tâches #17-20
- #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>
This commit is contained in:
456
api/scripts/cron/aggregate_event_stats.php
Executable file
456
api/scripts/cron/aggregate_event_stats.php
Executable file
@@ -0,0 +1,456 @@
|
||||
#!/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);
|
||||
Reference in New Issue
Block a user